From 1f1b18441157e9a8c27125810e8fd14b5641343e Mon Sep 17 00:00:00 2001 From: yanpeng200212 <32453@DESKTOP-HTJ2T0B> Date: Tue, 27 Oct 2026 09:09:57 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B7=A5=E8=B5=84/=E4=BB=93?= =?UTF-8?q?=E5=82=A8/=E7=94=9F=E4=BA=A7=E6=97=A5=E6=8A=A5=E7=AD=89?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=EF=BC=8C=E7=A7=BB=E9=99=A4=E6=97=A7=E8=80=83?= =?UTF-8?q?=E5=8B=A4=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 PayRoll(工资)、Wms(仓储)、Prod(车速/缫丝/考勤/丝片检测/丝片台账/纤检日报/质量日报) 模块 - 新增对应 Domain 实体、Application DTO/Service、Api Controller 与前端页面 - 移除旧 Attendance 考勤模块(Controller/Service/Entity/DTO 及 5 个前端页面),由 Prod/Kaoqin 取代 - 调整路由、仪表盘、Lsjl 流丝记录实体与服务 - 更新业务种子数据与 DbSeeder - 补充工作进度文档 14-27 --- .../Attendance/AttendanceController.cs | 103 - .../PayRoll/CalcLdcctController.cs | 66 + .../PayRoll/ConfEmployeeController.cs | 101 + .../PayRoll/PayRollConfController.cs | 230 ++ .../Controllers/Prod/CdsController.cs | 55 + .../Controllers/Prod/GlxdController.cs | 34 + .../Controllers/Prod/KaoqinController.cs | 100 + .../Controllers/Prod/SpjcController.cs | 54 + .../Controllers/Prod/SptzController.cs | 58 + .../Controllers/Prod/XjrbController.cs | 143 ++ .../Controllers/Prod/ZjrbController.cs | 90 + .../Controllers/Wms/WmsController.cs | 328 +++ .../F9WebAiTest.Api/f9_web_ai_test_rbac.db | Bin 6885376 -> 7344128 bytes .../DependencyInjection.cs | 27 +- .../Dtos/Attendance/AttendanceDtos.cs | 87 - .../Dtos/PayRoll/CalcLdcctDtos.cs | 131 ++ .../Dtos/PayRoll/ConfEmployeeDtos.cs | 40 + .../Dtos/PayRoll/PayRollConfDtos.cs | 204 ++ .../Dtos/Prod/HsDtos.cs | 105 + .../Dtos/Prod/HsTzDtos.cs | 129 ++ .../Dtos/Prod/KaoqinDtos.cs | 187 ++ .../Dtos/Prod/LsjlDtos.cs | 4 + .../Dtos/Prod/XjrbDtos.cs | 228 ++ .../Dtos/Prod/ZjrbDtos.cs | 111 + .../Dtos/Wms/WmsDtos.cs | 695 ++++++ .../Services/Attendance/AttendanceService.cs | 573 ----- .../Services/Attendance/IAttendanceService.cs | 26 - .../Services/PayRoll/CalcLdcctService.cs | 819 +++++++ .../Services/PayRoll/ConfEmployeeService.cs | 225 ++ .../Services/PayRoll/PayRollConfService.cs | 493 +++++ .../Services/Prod/CdsService.cs | 257 +++ .../Services/Prod/GlxdService.cs | 109 + .../Services/Prod/KaoqinService.cs | 890 ++++++++ .../Services/Prod/LsjlService.cs | 7 +- .../Services/Prod/SpjcService.cs | 188 ++ .../Services/Prod/SptzService.cs | 159 ++ .../Services/Prod/XjrbService.cs | 496 +++++ .../Services/Prod/ZjrbService.cs | 609 ++++++ .../Services/Wms/WmsService.cs | 1903 +++++++++++++++++ .../Entities/Attendance/AttendanceRecord.cs | 18 - .../Entities/Attendance/LeaveApprover.cs | 18 - .../Entities/Attendance/LeaveRequest.cs | 21 - .../Entities/PayRoll/CalcLdcct.cs | 55 + .../Entities/PayRoll/Conf.cs | 118 + .../Entities/PayRoll/ConfEmployee.cs | 45 + .../F9WebAiTest.Domain/Entities/Prod/Cds.cs | 32 + .../F9WebAiTest.Domain/Entities/Prod/Cqdj.cs | 55 + .../F9WebAiTest.Domain/Entities/Prod/Cqtj.cs | 74 + .../F9WebAiTest.Domain/Entities/Prod/Glxd.cs | 13 + .../Entities/Prod/LsjlRecord.cs | 2 + .../F9WebAiTest.Domain/Entities/Prod/Spjc.cs | 46 + .../F9WebAiTest.Domain/Entities/Prod/Sptz.cs | 29 + .../Entities/Prod/XjpbBanzu.cs | 32 + .../F9WebAiTest.Domain/Entities/Prod/Xjrb.cs | 113 + .../F9WebAiTest.Domain/Entities/Prod/Zjrb.cs | 79 + .../F9WebAiTest.Domain/Entities/Wms/Wms.cs | 378 ++++ .../Data/BusinessSeedData.cs | 14 + .../Data/DbSeeder.cs | 217 +- .../F9ReportDesignerHandler.ps1 | 117 + web/src/router/index.js | 39 +- web/src/views/attendance/approve/index.vue | 154 -- web/src/views/attendance/leave/index.vue | 141 -- web/src/views/attendance/my/index.vue | 160 -- web/src/views/attendance/punch/index.vue | 207 -- web/src/views/attendance/records/index.vue | 133 -- web/src/views/dashboard/index.vue | 1 - web/src/views/payroll/calcldcct/index.vue | 299 +++ web/src/views/payroll/confdcg/index.vue | 124 ++ web/src/views/payroll/confemployee/index.vue | 114 + web/src/views/payroll/confgyzk/index.vue | 182 ++ web/src/views/payroll/confgz/index.vue | 217 ++ web/src/views/payroll/confzq/index.vue | 91 + web/src/views/payroll/dcxdjk/index.vue | 126 ++ web/src/views/payroll/gwsrkh/index.vue | 133 ++ web/src/views/payroll/xzjg/index.vue | 123 ++ web/src/views/prod/cds/index.vue | 279 +++ web/src/views/prod/glxd/index.vue | 102 + web/src/views/prod/kaoqin/cqtj/index.vue | 312 +++ web/src/views/prod/kaoqin/index.vue | 608 ++++++ web/src/views/prod/lsjl/index.vue | 22 +- web/src/views/prod/spjc/index.vue | 287 +++ web/src/views/prod/sptz/index.vue | 272 +++ .../views/prod/xuanjian/XjpbBanzuPanel.vue | 208 ++ web/src/views/prod/xuanjian/XjrbPanel.vue | 390 ++++ web/src/views/prod/xuanjian/XjrbPlanPanel.vue | 404 ++++ web/src/views/prod/xuanjian/index.vue | 30 + web/src/views/prod/zjrb/ZjrbEditDialog.vue | 531 +++++ web/src/views/prod/zjrb/index.vue | 354 +++ web/src/views/wms/components/ChukuPanel.vue | 715 +++++++ .../views/wms/components/PiciSelectDialog.vue | 278 +++ web/src/views/wms/components/RukuPanel.vue | 722 +++++++ web/src/views/wms/components/SsPici.vue | 528 +++++ .../views/wms/components/TaizhangPanel.vue | 295 +++ web/src/views/wms/components/WjCgbx.vue | 102 + web/src/views/wms/components/WjCgplan.vue | 240 +++ web/src/views/wms/components/WjCrk.vue | 90 + web/src/views/wms/components/WjKucun.vue | 54 + web/src/views/wms/components/YlCaigou.vue | 191 ++ web/src/views/wms/components/YlFbplan.vue | 262 +++ web/src/views/wms/fuchanpin/index.vue | 44 + web/src/views/wms/moduleConfig.js | 153 ++ web/src/views/wms/ss/index.vue | 46 + web/src/views/wms/wj/index.vue | 52 + web/src/views/wms/yuanliao/index.vue | 48 + web/src/views/wms/zjp/index.vue | 45 + 工作进度/工作进度-14.md | 143 ++ 工作进度/工作进度-15.md | 162 ++ 工作进度/工作进度-16.md | 211 ++ 工作进度/工作进度-17.md | 233 ++ 工作进度/工作进度-18.md | 170 ++ 工作进度/工作进度-19.md | 153 ++ 工作进度/工作进度-20.md | 76 + 工作进度/工作进度-21.md | 74 + 工作进度/工作进度-22.md | 75 + 工作进度/工作进度-23.md | 149 ++ 工作进度/工作进度-24.md | 78 + 工作进度/工作进度-25.md | 127 ++ 工作进度/工作进度-26.md | 106 + 工作进度/工作进度-27.md | 39 + 119 files changed, 21576 insertions(+), 1668 deletions(-) delete mode 100644 backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/PayRoll/CalcLdcctController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/PayRoll/ConfEmployeeController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/PayRoll/PayRollConfController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/CdsController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/GlxdController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/KaoqinController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/SpjcController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/SptzController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/XjrbController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Prod/ZjrbController.cs create mode 100644 backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs delete mode 100644 backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/PayRoll/CalcLdcctDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/PayRoll/ConfEmployeeDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/PayRoll/PayRollConfDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/HsDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/HsTzDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/KaoqinDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/XjrbDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Prod/ZjrbDtos.cs create mode 100644 backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs delete mode 100644 backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs delete mode 100644 backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs create mode 100644 backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs create mode 100644 backend/F9WebAiTest.Application/Services/PayRoll/ConfEmployeeService.cs create mode 100644 backend/F9WebAiTest.Application/Services/PayRoll/PayRollConfService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/CdsService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/GlxdService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/SpjcService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/SptzService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/XjrbService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Prod/ZjrbService.cs create mode 100644 backend/F9WebAiTest.Application/Services/Wms/WmsService.cs delete mode 100644 backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs delete mode 100644 backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs delete mode 100644 backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/PayRoll/CalcLdcct.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/PayRoll/Conf.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/PayRoll/ConfEmployee.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Cds.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Cqdj.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Cqtj.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Glxd.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Spjc.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Sptz.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/XjpbBanzu.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Xjrb.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Prod/Zjrb.cs create mode 100644 backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs create mode 100644 tools/report-designer/F9ReportDesignerHandler.ps1 delete mode 100644 web/src/views/attendance/approve/index.vue delete mode 100644 web/src/views/attendance/leave/index.vue delete mode 100644 web/src/views/attendance/my/index.vue delete mode 100644 web/src/views/attendance/punch/index.vue delete mode 100644 web/src/views/attendance/records/index.vue create mode 100644 web/src/views/payroll/calcldcct/index.vue create mode 100644 web/src/views/payroll/confdcg/index.vue create mode 100644 web/src/views/payroll/confemployee/index.vue create mode 100644 web/src/views/payroll/confgyzk/index.vue create mode 100644 web/src/views/payroll/confgz/index.vue create mode 100644 web/src/views/payroll/confzq/index.vue create mode 100644 web/src/views/payroll/dcxdjk/index.vue create mode 100644 web/src/views/payroll/gwsrkh/index.vue create mode 100644 web/src/views/payroll/xzjg/index.vue create mode 100644 web/src/views/prod/cds/index.vue create mode 100644 web/src/views/prod/glxd/index.vue create mode 100644 web/src/views/prod/kaoqin/cqtj/index.vue create mode 100644 web/src/views/prod/kaoqin/index.vue create mode 100644 web/src/views/prod/spjc/index.vue create mode 100644 web/src/views/prod/sptz/index.vue create mode 100644 web/src/views/prod/xuanjian/XjpbBanzuPanel.vue create mode 100644 web/src/views/prod/xuanjian/XjrbPanel.vue create mode 100644 web/src/views/prod/xuanjian/XjrbPlanPanel.vue create mode 100644 web/src/views/prod/xuanjian/index.vue create mode 100644 web/src/views/prod/zjrb/ZjrbEditDialog.vue create mode 100644 web/src/views/prod/zjrb/index.vue create mode 100644 web/src/views/wms/components/ChukuPanel.vue create mode 100644 web/src/views/wms/components/PiciSelectDialog.vue create mode 100644 web/src/views/wms/components/RukuPanel.vue create mode 100644 web/src/views/wms/components/SsPici.vue create mode 100644 web/src/views/wms/components/TaizhangPanel.vue create mode 100644 web/src/views/wms/components/WjCgbx.vue create mode 100644 web/src/views/wms/components/WjCgplan.vue create mode 100644 web/src/views/wms/components/WjCrk.vue create mode 100644 web/src/views/wms/components/WjKucun.vue create mode 100644 web/src/views/wms/components/YlCaigou.vue create mode 100644 web/src/views/wms/components/YlFbplan.vue create mode 100644 web/src/views/wms/fuchanpin/index.vue create mode 100644 web/src/views/wms/moduleConfig.js create mode 100644 web/src/views/wms/ss/index.vue create mode 100644 web/src/views/wms/wj/index.vue create mode 100644 web/src/views/wms/yuanliao/index.vue create mode 100644 web/src/views/wms/zjp/index.vue create mode 100644 工作进度/工作进度-14.md create mode 100644 工作进度/工作进度-15.md create mode 100644 工作进度/工作进度-16.md create mode 100644 工作进度/工作进度-17.md create mode 100644 工作进度/工作进度-18.md create mode 100644 工作进度/工作进度-19.md create mode 100644 工作进度/工作进度-20.md create mode 100644 工作进度/工作进度-21.md create mode 100644 工作进度/工作进度-22.md create mode 100644 工作进度/工作进度-23.md create mode 100644 工作进度/工作进度-24.md create mode 100644 工作进度/工作进度-25.md create mode 100644 工作进度/工作进度-26.md create mode 100644 工作进度/工作进度-27.md diff --git a/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs b/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs deleted file mode 100644 index 2ed70c5..0000000 --- a/backend/F9WebAiTest.Api/Controllers/Attendance/AttendanceController.cs +++ /dev/null @@ -1,103 +0,0 @@ -using F9WebAiTest.Application.Common; -using F9WebAiTest.Application.Dtos; -using F9WebAiTest.Application.Dtos.Attendance; -using F9WebAiTest.Application.Services; -using F9WebAiTest.Application.Services.Attendance; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace F9WebAiTest.Api.Controllers.Attendance; - -[ApiController] -[Route("api/attendance")] -[Authorize] -public class AttendanceController : ControllerBase -{ - private readonly IAttendanceService _service; - - public AttendanceController(IAttendanceService service) - { - _service = service; - } - - // POST /api/attendance/punch 打卡 - [HttpPost("punch")] - public async Task Punch() - { - var result = await _service.PunchAsync(GetUserId()); - return Ok(ApiResult.Ok(result)); - } - - // GET /api/attendance/my 我的考勤记录 - [HttpGet("my")] - public async Task GetMy([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 10) - { - var result = await _service.GetMyRecordsAsync(GetUserId(), pageIndex, pageSize); - return Ok(ApiResult>.Ok(result)); - } - - // GET /api/attendance/my/summary 我的考勤统计 - [HttpGet("my/summary")] - public async Task GetMySummary([FromQuery] string month) - { - var result = await _service.GetMySummaryAsync(GetUserId(), month); - return Ok(ApiResult.Ok(result)); - } - - // GET /api/attendance 全部考勤(管理员) - [HttpGet] - public async Task GetAll([FromQuery] AttendanceQueryDto query) - { - var result = await _service.GetAllRecordsAsync(query); - return Ok(ApiResult>.Ok(result)); - } - - // POST /api/attendance/leave 申请请假 - [HttpPost("leave")] - public async Task ApplyLeave([FromBody] CreateLeaveDto dto) - { - try - { - var result = await _service.ApplyLeaveAsync(GetUserId(), dto); - return Ok(ApiResult.Ok(result, "申请已提交")); - } - catch (ServiceException ex) - { - return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); - } - } - - // GET /api/attendance/leave/my 我的请假 - [HttpGet("leave/my")] - public async Task GetMyLeaves() - { - var result = await _service.GetMyLeavesAsync(GetUserId()); - return Ok(ApiResult>.Ok(result)); - } - - // GET /api/attendance/leave 全部请假(管理员) - [HttpGet("leave")] - public async Task GetAllLeaves([FromQuery] string status) - { - var result = await _service.GetAllLeavesAsync(status); - return Ok(ApiResult>.Ok(result)); - } - - // PUT /api/attendance/leave/{id}/approve 审批请假 - [HttpPut("leave/{id}/approve")] - public async Task ApproveLeave(long id, [FromBody] ApproveLeaveDto dto) - { - try - { - var approverName = User.FindFirst("realName")?.Value ?? "管理员"; - var result = await _service.ApproveLeaveAsync(id, GetUserId(), approverName, dto); - return Ok(ApiResult.Ok(result, "审批完成")); - } - catch (ServiceException ex) - { - return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); - } - } - - private long GetUserId() => long.Parse(User.FindFirst("id")!.Value); -} diff --git a/backend/F9WebAiTest.Api/Controllers/PayRoll/CalcLdcctController.cs b/backend/F9WebAiTest.Api/Controllers/PayRoll/CalcLdcctController.cs new file mode 100644 index 0000000..b761824 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/PayRoll/CalcLdcctController.cs @@ -0,0 +1,66 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Application.Services.PayRoll; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.PayRoll; + +/// 薪资计算-挡车/车头产量计算(对应 F9 薪资-挡车车头产量计算 CALCLDCCT) +[ApiController] +[Route("api/payroll/calcldcct")] +[Authorize] +public class CalcLdcctController : ControllerBase +{ + private readonly ICalcLdcctService _svc; + public CalcLdcctController(ICalcLdcctService svc) => _svc = svc; + + /// 列表(账期起止 + 部门 + 工种 + 姓名/庄口关键字) + [HttpGet("list")] + public async Task GetPaged([FromQuery] DateTime start, [FromQuery] DateTime end, + [FromQuery] long? depId = null, [FromQuery] long? worktypeId = null, + [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 50) + => Ok(ApiResult>.Ok( + await _svc.GetPagedAsync(pageIndex, pageSize, start, end, depId, worktypeId, like ?? string.Empty))); + + /// 一键计算(挡车工):整账期 + 部门 + 工种全量覆盖 + [HttpPost("calc-dcg")] + public async Task CalcDcg([FromBody] CalcLdcctCalcDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CalcDcgAsync(dto), "计算完成")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 一键计算(车头工):依赖已存的挡车工产量 + [HttpPost("calc-ctg")] + public async Task CalcCtg([FromBody] CalcLdcctCalcDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CalcCtgAsync(dto), "计算完成")); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 保存人工微调结果(整账期 + 部门 + 工种全量覆盖) + [HttpPost("save")] + public async Task Save([FromBody] CalcLdcctSaveDto dto) + { + try + { + var count = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { count }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 删除整账期 + 部门 + 工种的计算结果 + [HttpDelete] + public async Task Delete([FromQuery] DateTime start, [FromQuery] DateTime end, + [FromQuery] long depId, [FromQuery] long worktypeId) + { + try + { + await _svc.DeleteAsync(start, end, depId, worktypeId); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/PayRoll/ConfEmployeeController.cs b/backend/F9WebAiTest.Api/Controllers/PayRoll/ConfEmployeeController.cs new file mode 100644 index 0000000..f203bfe --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/PayRoll/ConfEmployeeController.cs @@ -0,0 +1,101 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Application.Services.PayRoll; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.PayRoll; + +/// 薪资配置-账期(CONFZQ):考勤统计的「前缫出勤天数」 +[ApiController] +[Route("api/payroll/confzq")] +[Authorize] +public class ConfZqController : ControllerBase +{ + private readonly IConfZqService _svc; + public ConfZqController(IConfZqService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + [HttpGet("")] + public async Task List([FromQuery] int? year = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(year))); + + [HttpGet("{year:int}/{month:int}")] + public async Task Get(int year, int month) + => Ok(ApiResult.Ok(await _svc.GetByYearMonthAsync(year, month))); + + [HttpPost("")] + public async Task Save([FromBody] ConfZqDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } +} + +/// 薪资配置-职工(CONFEMPLOYEE):法定假小时数 / 是否学工 +[ApiController] +[Route("api/payroll/confemployee")] +[Authorize] +public class ConfEmployeeController : ControllerBase +{ + private readonly IConfEmployeeService _svc; + public ConfEmployeeController(IConfEmployeeService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + [HttpGet("")] + public async Task List([FromQuery] long? employeeId = null, [FromQuery] long? worktypeId = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(employeeId, worktypeId))); + + [HttpPost("")] + public async Task Save([FromBody] ConfEmployeeDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } +} + +/// 岗位胜任考核:考核结果决定加班费单价档位 +[ApiController] +[Route("api/payroll/gwsrkh")] +[Authorize] +public class GwsrkhController : ControllerBase +{ + private readonly IGwsrkhService _svc; + public GwsrkhController(IGwsrkhService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + [HttpGet("")] + public async Task List([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId = 0) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(year, month, depId))); + + [HttpPost("")] + public async Task Save([FromBody] GwsrkhDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try { await _svc.DeleteAsync(id); return Ok(ApiResult.Ok(null, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/PayRoll/PayRollConfController.cs b/backend/F9WebAiTest.Api/Controllers/PayRoll/PayRollConfController.cs new file mode 100644 index 0000000..2270a6b --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/PayRoll/PayRollConfController.cs @@ -0,0 +1,230 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Application.Services.PayRoll; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.PayRoll; + +// ==================== 薪资配置-工艺庄口 ==================== + +/// 薪资配置-工艺庄口(挡车车头产量计算的丝量/单价/系数基准) +[ApiController] +[Route("api/payroll/confgyzk")] +[Authorize] +public class ConfGyzkController : ControllerBase +{ + private readonly IConfGyzkService _svc; + public ConfGyzkController(IConfGyzkService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList([FromQuery] int? year = null, [FromQuery] int? month = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(year, month))); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] ConfGyzkSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 薪资配置-工种 ==================== + +/// 薪资配置-工种/岗位(标准上班时长、标准出勤天数及各项金额) +[ApiController] +[Route("api/payroll/confgz")] +[Authorize] +public class ConfGzController : ControllerBase +{ + private readonly IConfGzService _svc; + public ConfGzController(IConfGzService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList() => Ok(ApiResult>.Ok(await _svc.GetListAsync())); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] ConfGzSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 薪资配置-挡车/车头岗位 ==================== + +/// 薪资配置-挡车/车头岗位(按绪数档的岗位基数金额、车头工金额) +[ApiController] +[Route("api/payroll/confdcg")] +[Authorize] +public class ConfDcgController : ControllerBase +{ + private readonly IConfDcgService _svc; + public ConfDcgController(IConfDcgService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList() => Ok(ApiResult>.Ok(await _svc.GetListAsync())); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] ConfDcgSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 挡车纤度奖扣 ==================== + +/// 挡车纤度奖扣(按规格 + 纤度配置野纤的奖励/扣款) +[ApiController] +[Route("api/payroll/dcxdjk")] +[Authorize] +public class DcxdjkController : ControllerBase +{ + private readonly IDcxdjkService _svc; + public DcxdjkController(IDcxdjkService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList([FromQuery] string guige = "") + => Ok(ApiResult>.Ok(await _svc.GetListAsync(guige ?? string.Empty))); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] DcxdjkSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} + +// ==================== 薪资价格配置 ==================== + +/// 薪资价格配置(如「薪资.前缫」下的挡车工/车头工计件等级奖扣单价) +[ApiController] +[Route("api/payroll/xzjg")] +[Authorize] +public class XzjgController : ControllerBase +{ + private readonly IXzjgService _svc; + public XzjgController(IXzjgService svc) => _svc = svc; + + [HttpGet("list")] + public async Task GetList([FromQuery] string group = "") + => Ok(ApiResult>.Ok(await _svc.GetListAsync(group ?? string.Empty))); + + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] XzjgSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/CdsController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/CdsController.cs new file mode 100644 index 0000000..b3dec22 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/CdsController.cs @@ -0,0 +1,55 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 秤大丝(生产-后缫):以落丝记录为主表,按车台录入丝量 +[ApiController] +[Route("api/prod/cds")] +[Authorize] +public class CdsController : ControllerBase +{ + private readonly ICdsService _svc; + public CdsController(ICdsService svc) => _svc = svc; + + /// 秤大丝列表(落丝记录 + 秤丝汇总) + [HttpGet("list")] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, + [FromQuery] DateTime? date = null, [FromQuery] long? deptId = null) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId))); + + /// 称重弹窗数据(车台明细;首次称重按车台范围初始化) + [HttpGet("{lsjlId}")] + public async Task GetEdit(long lsjlId) + { + try { return Ok(ApiResult.Ok(await _svc.GetEditAsync(lsjlId))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 保存秤大丝(车台明细先删后建 + 汇总指标) + [HttpPost("save")] + public async Task Save([FromBody] CdsSaveDto dto) + { + try + { + await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 清除称重数据(汇总 + 车台明细) + [HttpDelete("{lsjlId}")] + public async Task Delete(long lsjlId) + { + try + { + await _svc.DeleteAsync(lsjlId); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/GlxdController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/GlxdController.cs new file mode 100644 index 0000000..263821a --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/GlxdController.cs @@ -0,0 +1,34 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 公量纤度(生产-后缫):按「日期 + 车间」整日批量录入 +[ApiController] +[Route("api/prod/glxd")] +[Authorize] +public class GlxdController : ControllerBase +{ + private readonly IGlxdService _svc; + public GlxdController(IGlxdService svc) => _svc = svc; + + /// 公量纤度列表(落丝记录 + 纤度检验平均纤度 + 公量纤度) + [HttpGet("list")] + public async Task GetList([FromQuery] DateTime? date = null, [FromQuery] long? deptId = null) + => Ok(ApiResult>.Ok(await _svc.GetListAsync(date, deptId))); + + /// 批量保存公量纤度 + [HttpPost("save")] + public async Task Save([FromBody] GlxdSaveDto dto) + { + try + { + await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/KaoqinController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/KaoqinController.cs new file mode 100644 index 0000000..d681c1d --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/KaoqinController.cs @@ -0,0 +1,100 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// +/// 生产-考勤:考勤登记(CQDJ)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ。 +/// +[ApiController] +[Route("api/prod/kaoqin")] +[Authorize] +public class KaoqinController : ControllerBase +{ + private readonly IKaoqinService _svc; + public KaoqinController(IKaoqinService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + /// 整组登记查询(日期 + 部门 + 工种 + 班次) + [HttpGet("cqdj")] + public async Task GetRegister([FromQuery] DateTime date, [FromQuery] long depId, + [FromQuery] long worktypeId = 0, [FromQuery] int banci = 1, [FromQuery] string keyword = "") + { + try { return Ok(ApiResult.Ok(await _svc.GetRegisterAsync(date, depId, worktypeId, banci, keyword ?? ""))); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 整组覆盖保存 + [HttpPost("cqdj")] + public async Task SaveRegister([FromBody] CqdjSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.SaveRegisterAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 删除整组登记 + [HttpDelete("cqdj")] + public async Task DeleteRegister([FromQuery] DateTime date, [FromQuery] long depId, + [FromQuery] long worktypeId = 0, [FromQuery] int banci = 1) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.DeleteRegisterAsync(date, depId, worktypeId, banci) }, "删除成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 「添加职工」可选职工 + [HttpGet("cqdj/employees")] + public async Task Employees([FromQuery] long depId, [FromQuery] long worktypeId = 0, [FromQuery] int banci = 1) + => Ok(ApiResult>.Ok(await _svc.GetEmployeesAsync(depId, worktypeId, banci))); + + // ==================== 考勤统计(CQTJ) ==================== + + /// 统计分页(库中的月快照) + [HttpGet("cqtj")] + public async Task CqtjList([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId = 0, + [FromQuery] long worktypeId = 0, [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetCqtjPagedAsync(year, month, depId, worktypeId, like ?? "", pageIndex, pageSize))); + + /// 录入:已有快照取库,否则按账期实时汇总 + [HttpGet("cqtj/edit")] + public async Task CqtjEdit([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId) + { + try { return Ok(ApiResult.Ok(await _svc.GetCqtjForEditAsync(year, month, depId))); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 保存(整组覆盖) + [HttpPost("cqtj")] + public async Task CqtjSave([FromBody] CqtjSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.SaveCqtjAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 删除该年月的统计快照(depId=0 表示全部部门) + [HttpDelete("cqtj")] + public async Task CqtjDelete([FromQuery] int year, [FromQuery] int month, [FromQuery] long depId = 0) + { + try + { + var count = depId > 0 + ? await _svc.DeleteCqtjAsync(year, month, depId) + : await _svc.DeleteCqtjAllAsync(year, month); + return Ok(ApiResult.Ok(new { count }, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 一键生成:遍历全部车间,缺的才生成 + [HttpPost("cqtj/generate")] + public async Task CqtjGenerate([FromBody] CqtjGenerateDto dto) + { + try { return Ok(ApiResult.Ok(new { count = await _svc.GenerateCqtjAllAsync(dto.Year, dto.Month) }, "生成完成")); } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/SpjcController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/SpjcController.cs new file mode 100644 index 0000000..e1003a8 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/SpjcController.cs @@ -0,0 +1,54 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 丝片检查(生产-后缫):按「日期 + 车间 + 工艺庄口 + 规格」录入疵点片数 +[ApiController] +[Route("api/prod/spjc")] +[Authorize] +public class SpjcController : ControllerBase +{ + private readonly ISpjcService _svc; + public SpjcController(ISpjcService svc) => _svc = svc; + + /// 丝片检查列表(日期 + 车间 + 关键字) + [HttpGet("list")] + public async Task GetPaged([FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20, + [FromQuery] DateTime? date = null, [FromQuery] long? deptId = null, [FromQuery] string like = "") + => Ok(ApiResult>.Ok( + await _svc.GetPagedAsync(pageIndex, pageSize, date, deptId, like ?? string.Empty))); + + /// 修改回填 + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpPost("save")] + public async Task Save([FromBody] SpjcSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/SptzController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/SptzController.cs new file mode 100644 index 0000000..ef36955 --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/SptzController.cs @@ -0,0 +1,58 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 丝片台帐(生产-后缫):每日一条,按账期区间查询 +[ApiController] +[Route("api/prod/sptz")] +[Authorize] +public class SptzController : ControllerBase +{ + private readonly ISptzService _svc; + public SptzController(ISptzService svc) => _svc = svc; + + /// 丝片台帐列表(账期起止) + [HttpGet("list")] + public async Task GetPaged([FromQuery] DateTime dateFrom, [FromQuery] DateTime dateTo, + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 31) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(pageIndex, pageSize, dateFrom, dateTo))); + + /// 修改回填 + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 新增默认值:上一本台账 + 上存,并按当日「丝片检查」带出整理片数 + [HttpGet("defaults")] + public async Task GetDefaults([FromQuery] DateTime date) + => Ok(ApiResult.Ok(await _svc.GetDefaultsAsync(date))); + + [HttpPost("save")] + public async Task Save([FromBody] SptzSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/XjrbController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/XjrbController.cs new file mode 100644 index 0000000..895dd0c --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/XjrbController.cs @@ -0,0 +1,143 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// +/// 生产-选剥茧(对应 F9 WinClient\F9.Business.Prod\XBJ): +/// 选茧日报(含盈亏 / 联统率)、选茧计划及其明细、选茧班组与班组职工。 +/// +[ApiController] +[Route("api/prod/xjrb")] +[Authorize] +public class XjrbController : ControllerBase +{ + private readonly IXjrbService _svc; + public XjrbController(IXjrbService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + // ==================== 选茧日报 ==================== + + [HttpGet("")] + public async Task List([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, + [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok(await _svc.GetPagedAsync(start, end, like ?? "", pageIndex, pageSize))); + + [HttpGet("{id:long}")] + public async Task Get(long id) + => Ok(ApiResult.Ok(await _svc.GetByIdAsync(id))); + + [HttpPost("")] + public async Task Save([FromBody] XjrbSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SaveAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("{id:long}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 计算盈亏 / 联统率(前端保存前调用,把结果写入表单) + [HttpPost("count-yingkui")] + public async Task CountYingkui([FromBody] XjrbSaveDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CountYingkuiAsync(dto))); } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 选茧计划 ==================== + + [HttpGet("plan")] + public async Task PlanList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, + [FromQuery] string like = "", [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok(await _svc.GetPlanPagedAsync(start, end, like ?? "", pageIndex, pageSize))); + + [HttpGet("plan/{id:long}")] + public async Task PlanGet(long id) + => Ok(ApiResult.Ok(await _svc.GetPlanByIdAsync(id))); + + [HttpPost("plan")] + public async Task PlanSave([FromBody] XjrbPlanSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SavePlanAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("plan/{id:long}")] + public async Task PlanDelete(long id) + { + try + { + await _svc.DeletePlanAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 选茧计划明细 ==================== + + [HttpGet("plan/{planId:long}/items")] + public async Task PlanItems(long planId) + => Ok(ApiResult>.Ok(await _svc.GetPlanItemsAsync(planId))); + + [HttpPost("plan/item")] + public async Task PlanItemSave([FromBody] XjrbItemSaveDto dto) + { + try { return Ok(ApiResult.Ok(new { id = await _svc.SavePlanItemAsync(dto) }, "保存成功")); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("plan/item/{id:long}")] + public async Task PlanItemDelete(long id) + { + try + { + await _svc.DeletePlanItemAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 计划维度盈亏(联统率按包数口径) + [HttpPost("plan/count-yingkui")] + public async Task PlanCountYingkui([FromBody] XjrbItemSaveDto dto) + { + try { return Ok(ApiResult.Ok(await _svc.CountPlanYingkuiAsync(dto))); } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 选茧班组 ==================== + + [HttpGet("banzu")] + public async Task BanzuList([FromQuery] string banci = "", [FromQuery] int? zu = null, + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok(await _svc.GetBanzuPagedAsync(banci ?? "", zu, pageIndex, pageSize))); + + [HttpGet("banzu/{id:long}/zhigong")] + public async Task BanzuZhigong(long id) + => Ok(ApiResult>.Ok(await _svc.GetBanzuZhigongAsync(id))); + + /// 班组「编辑录入」:按班次整体覆盖保存职工与组别 + [HttpPost("banzu/zhigong")] + public async Task BanzuZhigongSave([FromBody] List dtos) + { + try + { + await _svc.SaveBanzuZhigongAsync(dtos ?? new List()); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Prod/ZjrbController.cs b/backend/F9WebAiTest.Api/Controllers/Prod/ZjrbController.cs new file mode 100644 index 0000000..3e5d9dc --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Prod/ZjrbController.cs @@ -0,0 +1,90 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Application.Services.Prod; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Prod; + +/// 煮茧日报(生产-前缫):主表 + 机组 / 班次 / 机组班次 +[ApiController] +[Route("api/prod/zjrb")] +[Authorize] +public class ZjrbController : ControllerBase +{ + private readonly IZjrbService _svc; + public ZjrbController(IZjrbService svc) => _svc = svc; + + // ---------- 查询 ---------- + + /// 日报列表(账期起止 + 车间 + 庄口/规格关键字) + [HttpGet("list")] + public async Task GetPaged([FromQuery] DateTime dateFrom, [FromQuery] DateTime dateTo, + [FromQuery] long depId = 0, [FromQuery] string like = "", [FromQuery] int pageIndex = 1, + [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetPagedAsync(dateFrom, dateTo, depId, like ?? string.Empty, pageIndex, pageSize))); + + /// 某条日报的机组明细 + [HttpGet("{id}/jizus")] + public async Task GetJizus(long id) + { + try { return Ok(ApiResult>.Ok(await _svc.GetJizuListAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 修改回填(主表 + 班次 + 机组 + 机组班次) + [HttpGet("{id}")] + public async Task GetForEdit(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetForEditAsync(id))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + /// 新增默认值:按上一日结存带出上存、每桶茧量、工艺落丝桶数,并按机组车台生成机组行 + [HttpGet("defaults")] + public async Task GetDefaults([FromQuery] DateTime date, [FromQuery] long depId, + [FromQuery] long gyzkId, [FromQuery] string guige = "") + { + try { return Ok(ApiResult.Ok(await _svc.BuildDefaultsAsync(date, depId, gyzkId, guige))); } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // ---------- 保存 / 删除 ---------- + + [HttpPost("save")] + public async Task Save([FromBody] ZjrbSaveDto dto) + { + try + { + var id = await _svc.SaveAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + [HttpDelete("{id}")] + public async Task Delete(long id) + { + try + { + await _svc.DeleteAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } + + // ---------- 计算缫丝时长 ---------- + + /// 按「日期 + 车间」整日重算机组缫丝时长(单班 9 小时 / 双班 18 小时,按茧量比例分配) + [HttpPost("calculate-saosi-hours")] + public async Task CalculateSaosiHours([FromQuery] DateTime date, [FromQuery] long depId) + { + try + { + var changed = await _svc.CalculateJizuSaosiHoursAsync(date, depId); + return Ok(ApiResult.Ok(changed, changed > 0 ? $"已更新 {changed} 条机组缫丝时长" : "缫丝时长无变化")); + } + catch (ServiceException ex) { return StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); } + } +} diff --git a/backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs b/backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs new file mode 100644 index 0000000..743ee8b --- /dev/null +++ b/backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs @@ -0,0 +1,328 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Wms; +using F9WebAiTest.Application.Services.Wms; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace F9WebAiTest.Api.Controllers.Wms; + +/// +/// 仓储(WMS):原料(1) / 副产品(2) / 中间品(3) / 生丝(4) / 物资(5) +/// 对应 F9 WinClient\F9.Business.WMS 五个模块的入库 / 出库 / 库存台账及其扩展功能。 +/// +[ApiController] +[Route("api/wms")] +[Authorize] +public class WmsController : ControllerBase +{ + private readonly IWmsService _svc; + public WmsController(IWmsService svc) => _svc = svc; + + private IActionResult Fail(ServiceException ex) => StatusCode(ex.Code, ApiResult.Fail(ex.Message, ex.Code)); + + /// 下拉选项:单据类型 / 物资 / 原料庄口 / 工艺庄口 / 部门 / 往来单位 + [HttpGet("options")] + public async Task Options([FromQuery] int module) + => Ok(ApiResult.Ok(await _svc.GetOptionsAsync(module))); + + // ==================== 入库 ==================== + + [HttpGet("ruku")] + public async Task RukuList([FromQuery] int module, [FromQuery] DateTime? start = null, + [FromQuery] DateTime? end = null, [FromQuery] int? type = null, [FromQuery] string like = "", + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetRukuPagedAsync(module, start, end, type, like ?? "", pageIndex, pageSize))); + + [HttpGet("ruku/{id:long}")] + public async Task RukuGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetRukuAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("ruku")] + public async Task RukuSave([FromBody] WmsRukuSaveDto dto) + { + try + { + var id = await _svc.SaveRukuAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("ruku/{id:long}")] + public async Task RukuDelete(long id) + { + try + { + await _svc.DeleteRukuAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 采购报销(物资入库单置为已报销) + [HttpPost("ruku/baoxiao")] + public async Task Baoxiao([FromBody] List ids) + { + try + { + var count = await _svc.BaoxiaoAsync(ids ?? new List()); + return Ok(ApiResult.Ok(new { count }, "报销成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 出库 ==================== + + [HttpGet("chuku")] + public async Task ChukuList([FromQuery] int module, [FromQuery] DateTime? start = null, + [FromQuery] DateTime? end = null, [FromQuery] int? type = null, [FromQuery] string like = "", + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetChukuPagedAsync(module, start, end, type, like ?? "", pageIndex, pageSize))); + + [HttpGet("chuku/{id:long}")] + public async Task ChukuGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetChukuAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("chuku")] + public async Task ChukuSave([FromBody] WmsChukuSaveDto dto) + { + try + { + var id = await _svc.SaveChukuAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("chuku/{id:long}")] + public async Task ChukuDelete(long id) + { + try + { + await _svc.DeleteChukuAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 库存台账 ==================== + + [HttpGet("taizhang/dates")] + public async Task TaizhangDates([FromQuery] int module, [FromQuery] int kind = 0) + => Ok(ApiResult>.Ok(await _svc.GetTzDatesAsync(module, kind))); + + [HttpGet("taizhang")] + public async Task Taizhang([FromQuery] int module, [FromQuery] DateTime date, [FromQuery] int kind = 0, + [FromQuery] bool regenerate = false) + { + try { return Ok(ApiResult.Ok(await _svc.GetTaizhangAsync(module, kind, date, regenerate))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("taizhang")] + public async Task TaizhangSave([FromBody] WmsTzSaveDto dto) + { + try + { + var id = await _svc.SaveTaizhangAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("taizhang/{id:long}")] + public async Task TaizhangDelete(long id) + { + try + { + await _svc.DeleteTaizhangAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 库存 / 出入库流水 ==================== + + [HttpGet("kucun")] + public async Task Kucun([FromQuery] int module, [FromQuery] string like = "") + => Ok(ApiResult>.Ok(await _svc.GetKucunAsync(module, like ?? ""))); + + [HttpGet("crk")] + public async Task Crk([FromQuery] int module, [FromQuery] DateTime start, [FromQuery] DateTime end, + [FromQuery] long? wzmlId = null) + => Ok(ApiResult>.Ok(await _svc.GetCrkAsync(module, start, end, wzmlId))); + + // ==================== 生丝批次 / 包件 ==================== + + [HttpGet("pici")] + public async Task PiciList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, + [FromQuery] int kcStatus = 0, [FromQuery] string like = "", + [FromQuery] int pageIndex = 1, [FromQuery] int pageSize = 20) + => Ok(ApiResult>.Ok( + await _svc.GetPiciPagedAsync(start, end, kcStatus, like ?? "", pageIndex, pageSize))); + + [HttpGet("pici/{id:long}")] + public async Task PiciGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetPiciAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("pici")] + public async Task PiciSave([FromBody] WmsPiciSaveDto dto) + { + try + { + var id = await _svc.SavePiciAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("pici/{id:long}")] + public async Task PiciDelete(long id) + { + try + { + await _svc.DeletePiciAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 生丝出库选批次(批次 + 未出库包件) + [HttpGet("pici/chuku-select")] + public async Task PiciChukuSelect([FromQuery] long wzmlId = 0, [FromQuery] string excludePiciIds = "") + => Ok(ApiResult>.Ok(await _svc.GetPiciChukuSelectAsync(wzmlId, excludePiciIds ?? ""))); + + /// 批次质检 + [HttpGet("pici/qc/{piciId:long}")] + public async Task PiciQcGet(long piciId) + => Ok(ApiResult.Ok(await _svc.GetPiciQcAsync(piciId))); + + [HttpPost("pici/qc")] + public async Task PiciQcSave([FromBody] WmsPiciQcDto dto) + { + try + { + await _svc.SavePiciQcAsync(dto); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + /// 批次纤度检验 + [HttpGet("pici/xdjy/{piciId:long}")] + public async Task PiciQcXdjyGet(long piciId) + => Ok(ApiResult>.Ok(await _svc.GetPiciQcXdjyAsync(piciId))); + + [HttpPost("pici/xdjy")] + public async Task PiciQcXdjySave([FromBody] List items) + { + try + { + await _svc.SavePiciQcXdjyAsync(items ?? new List()); + return Ok(ApiResult.Ok(null, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 原料采购 ==================== + + [HttpGet("caigou")] + public async Task CaigouList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null) + => Ok(ApiResult>.Ok(await _svc.GetCaigouAsync(start, end))); + + [HttpPost("caigou")] + public async Task CaigouSave([FromBody] WmsCaigouDto dto) + { + try + { + var id = await _svc.SaveCaigouAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("caigou/{id:long}")] + public async Task CaigouDelete(long id) + { + try + { + await _svc.DeleteCaigouAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 原料翻包计划 ==================== + + [HttpGet("fbplan")] + public async Task FbplanList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null) + => Ok(ApiResult>.Ok(await _svc.GetFbplanAsync(start, end))); + + [HttpPost("fbplan")] + public async Task FbplanSave([FromBody] WmsFbplanDto dto) + { + try + { + var id = await _svc.SaveFbplanAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("fbplan/{id:long}")] + public async Task FbplanDelete(long id) + { + try + { + await _svc.DeleteFbplanAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + // ==================== 物资采购计划 ==================== + + [HttpGet("cgplan")] + public async Task CgplanList([FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null) + => Ok(ApiResult>.Ok(await _svc.GetCgplanAsync(start, end))); + + [HttpGet("cgplan/{id:long}")] + public async Task CgplanGet(long id) + { + try { return Ok(ApiResult.Ok(await _svc.GetCgplanAsync(id))); } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpPost("cgplan")] + public async Task CgplanSave([FromBody] WmsCgplanSaveDto dto) + { + try + { + var id = await _svc.SaveCgplanAsync(dto); + return Ok(ApiResult.Ok(new { id }, "保存成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } + + [HttpDelete("cgplan/{id:long}")] + public async Task CgplanDelete(long id) + { + try + { + await _svc.DeleteCgplanAsync(id); + return Ok(ApiResult.Ok(null, "删除成功")); + } + catch (ServiceException ex) { return Fail(ex); } + } +} diff --git a/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db b/backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db index a168e4238b08eed76a4b95ce95ceaf39f62563e2..86b7205382105b51e90c8429bcaa270ee02d6242 100644 GIT binary patch delta 162169 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 delta 4641 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 services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -29,6 +27,28 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + // 生产-选剥茧:选茧日报(含盈亏 / 联统率)/ 选茧计划及其明细 / 选茧班组及职工 + // (对齐 F9 WinClient\F9.Business.Prod\XBJ) + services.AddScoped(); + // 生产-后缫:秤大丝 / 公量纤度 / 丝片检查 / 丝片台帐(对齐 F9 WinClient\F9.Business.Prod\HS) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + // 生产-考勤:考勤登记 + 考勤统计(对齐 F9 WinClient\F9.Business.Prod\KQ\CQDJ / CQTJ) + services.AddScoped(); + // 薪资:挡车/车头产量计算及其依赖配置(对齐 F9 WinClient\F9.Business.PayRoll\PAYROLL) + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + // 考勤统计依赖:薪资配置-账期 / 薪资配置-职工 / 岗位胜任考核 + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -36,6 +56,9 @@ public static class DependencyInjection services.AddScoped(); services.AddScoped(); + // 仓储模块:原料 / 副产品 / 中间品 / 生丝 / 物资(对齐 F9 WinClient\F9.Business.WMS) + services.AddScoped(); + // 报表数据提供者(按 Config.DsName 解析,自动注册所有实现) foreach (var t in typeof(DependencyInjection).Assembly.GetTypes() .Where(t => !t.IsAbstract && typeof(IReportDataProvider).IsAssignableFrom(t))) diff --git a/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs b/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs deleted file mode 100644 index 3058471..0000000 --- a/backend/F9WebAiTest.Application/Dtos/Attendance/AttendanceDtos.cs +++ /dev/null @@ -1,87 +0,0 @@ -namespace F9WebAiTest.Application.Dtos.Attendance; - -public class AttendanceDto -{ - public long Id { get; set; } - public long EmployeeId { get; set; } - public string EmployeeName { get; set; } - public string DepartmentName { get; set; } - public DateTime WorkDate { get; set; } - public DateTime? SignInTime { get; set; } - public DateTime? SignOutTime { get; set; } - public string SignInStatus { get; set; } - public string SignOutStatus { get; set; } - public string Remark { get; set; } -} - -public class PunchResultDto -{ - public string Type { get; set; } // signIn / signOut - public DateTime Time { get; set; } - public string Status { get; set; } // 正常/迟到/早退 - public string Message { get; set; } -} - -public class AttendanceQueryDto -{ - public string Date { get; set; } = ""; // yyyy-MM-dd - public long? DepartmentId { get; set; } - public string Keyword { get; set; } = ""; - public int PageIndex { get; set; } = 1; - public int PageSize { get; set; } = 10; -} - -public class AttendanceSummaryDto -{ - public int NormalDays { get; set; } - public int LateDays { get; set; } - public int EarlyLeaveDays { get; set; } - public int AbsentDays { get; set; } - public int LeaveDays { get; set; } - public int TotalDays { get; set; } -} - -public class LeaveRequestDto -{ - public long Id { get; set; } - public long EmployeeId { get; set; } - public string EmployeeName { get; set; } - public string DepartmentName { get; set; } - public string Type { get; set; } - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public string Reason { get; set; } - public string Status { get; set; } - public int CurrentOrderNo { get; set; } // 当前审批到第几步 - public string ApproverName { get; set; } - public string ApproveRemark { get; set; } - public DateTime? ApprovedAt { get; set; } - public DateTime CreatedAt { get; set; } - public List Approvers { get; set; } = new(); // 审批流程节点 -} - -public class LeaveApproverDto -{ - public long Id { get; set; } - public long ApproverId { get; set; } - public string ApproverName { get; set; } - public int OrderNo { get; set; } // 序号(审批顺序) - public string Status { get; set; } // 待审批/已批准/已驳回 - public string ApproveRemark { get; set; } - public DateTime? ApprovedAt { get; set; } -} - -public class CreateLeaveDto -{ - public string Type { get; set; } = "事假"; - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public string Reason { get; set; } = ""; - public List ApproverIds { get; set; } = new(); // 审批人ID列表(顺序=审批顺序) -} - -public class ApproveLeaveDto -{ - public bool Approved { get; set; } - public string Remark { get; set; } = ""; -} diff --git a/backend/F9WebAiTest.Application/Dtos/PayRoll/CalcLdcctDtos.cs b/backend/F9WebAiTest.Application/Dtos/PayRoll/CalcLdcctDtos.cs new file mode 100644 index 0000000..a3bb28b --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/PayRoll/CalcLdcctDtos.cs @@ -0,0 +1,131 @@ +namespace F9WebAiTest.Application.Dtos.PayRoll; + +/// 挡车车头产量计算-列表行 +public class CalcLdcctListItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long EmployeeId { get; set; } + public string EmployeeName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public string Chetai { get; set; } = ""; + public int Jizu { get; set; } + public int Banci { get; set; } + public string BanciName { get; set; } = ""; + + public decimal HoursKantai { get; set; } // 看台时长 + public decimal Dangcheshu { get; set; } // 挡车数 + public string Dengji { get; set; } = ""; // 等级 + public decimal Dengjifen { get; set; } // 等级分 + + // 产量 + public decimal GyzkTaichan { get; set; } // 薪资台产(来自工艺庄口配置) + public decimal ClDanjiaBase { get; set; } + public decimal ClDanjiaDengji { get; set; } + public decimal ClDanjiaActual { get; set; } + public decimal ClSiliang { get; set; } + + // 质量 + public decimal GyzkSaozhe { get; set; } // 薪资缫折(工艺指标缫折) + public decimal ZlSaozhe { get; set; } + public decimal ZlCenterxd { get; set; } + public decimal ZlYexiannum { get; set; } + public decimal ZlQingjie { get; set; } + public decimal ZlJiejing { get; set; } + public decimal ZlErdu { get; set; } + public decimal ZlSandu { get; set; } + + // 金额 + public decimal JineCl_Original { get; set; } + public decimal Xishu_JineCl { get; set; } + public decimal JineCl { get; set; } + public decimal JineZlSaozhe { get; set; } + public decimal JineZlCenterxd { get; set; } + public decimal JineZlYexian { get; set; } + public decimal JineZlQingjie { get; set; } + public decimal JineZlJiejing { get; set; } + public decimal JineZlErdu { get; set; } + public decimal JineZlSandu { get; set; } + + /// 质量金额合计(负数为扣款) + public decimal JineZlTotal { get; set; } +} + +/// 挡车车头产量计算-保存行(整账期 + 部门 + 工种全量覆盖) +public class CalcLdcctSaveItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long EmployeeId { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } + public int Jizu { get; set; } + public int Banci { get; set; } + public long GyzkId { get; set; } + public string Guige { get; set; } = ""; + public string Chetai { get; set; } = ""; + + public decimal HoursKantai { get; set; } + public decimal Dangcheshu { get; set; } + public string Dengji { get; set; } = ""; + public decimal Dengjifen { get; set; } + + public decimal ClDanjiaBase { get; set; } + public decimal ClDanjiaDengji { get; set; } + public decimal ClDanjiaActual { get; set; } + public decimal ClSiliang { get; set; } + + public decimal ZlSaozhe { get; set; } + public decimal ZlCenterxd { get; set; } + public decimal ZlYexiannum { get; set; } + public decimal ZlQingjie { get; set; } + public decimal ZlJiejing { get; set; } + public decimal ZlErdu { get; set; } + public decimal ZlSandu { get; set; } + + public decimal JineCl_Original { get; set; } + public decimal Xishu_JineCl { get; set; } + public decimal JineCl { get; set; } + public decimal JineZlSaozhe { get; set; } + public decimal JineZlCenterxd { get; set; } + public decimal JineZlYexian { get; set; } + public decimal JineZlQingjie { get; set; } + public decimal JineZlJiejing { get; set; } + public decimal JineZlErdu { get; set; } + public decimal JineZlSandu { get; set; } +} + +/// 整账期保存请求 +public class CalcLdcctSaveDto +{ + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } + public List Items { get; set; } = new(); +} + +/// 计算请求(一键计算) +public class CalcLdcctCalcDto +{ + public DateTime StartDate { get; set; } + public DateTime EndDate { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } +} + +/// 计算结果 +public class CalcLdcctResultDto +{ + public int Count { get; set; } // 生成条数 + public decimal SumSiliang { get; set; } // 丝量合计(g) + public decimal SumJineCl { get; set; } // 产量金额合计 + public decimal SumJineZl { get; set; } // 质量金额合计 + public List Warnings { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/PayRoll/ConfEmployeeDtos.cs b/backend/F9WebAiTest.Application/Dtos/PayRoll/ConfEmployeeDtos.cs new file mode 100644 index 0000000..8c708b2 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/PayRoll/ConfEmployeeDtos.cs @@ -0,0 +1,40 @@ +namespace F9WebAiTest.Application.Dtos.PayRoll; + +// 考勤统计(CQTJ)依赖配置:薪资配置-账期 / 薪资配置-职工 / 岗位胜任考核 + +public class ConfZqDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public decimal QsQuanqinDays { get; set; } + public string Memo { get; set; } = ""; +} + +public class ConfEmployeeDto +{ + public long Id { get; set; } + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public decimal HoursFadingBz { get; set; } + public decimal HoursFadingGz { get; set; } + public bool IsXuegong { get; set; } + public string Memo { get; set; } = ""; +} + +public class GwsrkhDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + public string KhJieguo { get; set; } = ""; + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/PayRoll/PayRollConfDtos.cs b/backend/F9WebAiTest.Application/Dtos/PayRoll/PayRollConfDtos.cs new file mode 100644 index 0000000..c36fd18 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/PayRoll/PayRollConfDtos.cs @@ -0,0 +1,204 @@ +namespace F9WebAiTest.Application.Dtos.PayRoll; + +// ==================== 薪资配置-工艺庄口 ==================== + +public class ConfGyzkListItemDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public decimal QsTaichan { get; set; } + public string QsDengji { get; set; } = ""; + public decimal QsDengjifen { get; set; } + public decimal SaoZhe { get; set; } + public int MeiTongJianLiang { get; set; } + public int LuoSiTongShu { get; set; } + public decimal MeiCheSiLiang { get; set; } + public decimal DcgJijianXishu { get; set; } + public decimal CtgJijianXishu { get; set; } + public decimal GyzkLtwxKs { get; set; } + public decimal GyzkLtwxl { get; set; } + public decimal XjChanliangPrice { get; set; } +} + +public class ConfGyzkSaveDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long GyzkId { get; set; } + public string Guige { get; set; } = ""; + public decimal QsTaichan { get; set; } + public string QsDengji { get; set; } = ""; + public decimal QsDengjifen { get; set; } + public decimal SaoZhe { get; set; } + public int MeiTongJianLiang { get; set; } + public int LuoSiTongShu { get; set; } + public decimal MeiCheSiLiang { get; set; } + public decimal DcgJijianXishu { get; set; } + public decimal CtgJijianXishu { get; set; } + public decimal GyzkLtwxKs { get; set; } + public decimal GyzkLtwxl { get; set; } + public decimal XjChanliangPrice { get; set; } +} + +// ==================== 薪资配置-工种 ==================== + +public class ConfGzListItemDto +{ + public long Id { get; set; } + public long WorkTypeId { get; set; } + public string WorkTypeName { get; set; } = ""; + public decimal HoursPerday { get; set; } + public decimal QqDaysStandard { get; set; } + public decimal QqHoursMaxQj { get; set; } + public decimal QqHoursDkMinjiaban { get; set; } + public decimal QqHoursDkQingjia { get; set; } + public decimal XgJineQuanqin { get; set; } + public decimal XgJineGangwei { get; set; } + public decimal JineNiangong { get; set; } + public decimal JineQuanqin { get; set; } + public decimal JineGangwei { get; set; } + public decimal JineYuekhJixiao { get; set; } + public decimal JineGwkh { get; set; } + public decimal JineYueticheng { get; set; } + public decimal JineJtkhYear { get; set; } + public decimal JineJisuanjishu { get; set; } + public decimal JineYeban { get; set; } + public decimal JineJiaotongButie { get; set; } + public decimal LiudongJineGuding { get; set; } + public decimal JiaqiJineKuangongKk { get; set; } + public decimal JiaqiJineFadingGz { get; set; } + public decimal JiaqiJineFadingBz { get; set; } + public decimal JiaqiJineHunjiaBz { get; set; } + public decimal JiaqiJineSangjiaBz { get; set; } + public decimal JiaqiJineBingjiaBz { get; set; } + public decimal JiaqiJineLunxiuBz { get; set; } + public decimal JiabanJineBz { get; set; } + public decimal Jiaqi_Jine_TiaoxiuBz { get; set; } + public decimal Jiaqi_Jine_NianjiaBz { get; set; } + public decimal GwsrkhJineQianhege { get; set; } + public decimal GwsrkhJineQiwang { get; set; } + public decimal GwsrkhJineHege { get; set; } + public decimal GwsrkhJineShengren { get; set; } + public decimal GwsrkhJineChaoshengren { get; set; } + public decimal Jiaban_GwsrkhJineQianhege { get; set; } + public decimal Jiaban_GwsrkhJineQiwang { get; set; } + public decimal Jiaban_GwsrkhJineHege { get; set; } + public decimal Jiaban_GwsrkhJineShengren { get; set; } + public decimal Jiaban_GwsrkhJineChaoshengren { get; set; } +} + +public class ConfGzSaveDto +{ + public long Id { get; set; } + public long WorkTypeId { get; set; } + public decimal HoursPerday { get; set; } + public decimal QqDaysStandard { get; set; } + public decimal QqHoursMaxQj { get; set; } + public decimal QqHoursDkMinjiaban { get; set; } + public decimal QqHoursDkQingjia { get; set; } + public decimal XgJineQuanqin { get; set; } + public decimal XgJineGangwei { get; set; } + public decimal JineNiangong { get; set; } + public decimal JineQuanqin { get; set; } + public decimal JineGangwei { get; set; } + public decimal JineYuekhJixiao { get; set; } + public decimal JineGwkh { get; set; } + public decimal JineYueticheng { get; set; } + public decimal JineJtkhYear { get; set; } + public decimal JineJisuanjishu { get; set; } + public decimal JineYeban { get; set; } + public decimal JineJiaotongButie { get; set; } + public decimal LiudongJineGuding { get; set; } + public decimal JiaqiJineKuangongKk { get; set; } + public decimal JiaqiJineFadingGz { get; set; } + public decimal JiaqiJineFadingBz { get; set; } + public decimal JiaqiJineHunjiaBz { get; set; } + public decimal JiaqiJineSangjiaBz { get; set; } + public decimal JiaqiJineBingjiaBz { get; set; } + public decimal JiaqiJineLunxiuBz { get; set; } + public decimal JiabanJineBz { get; set; } + public decimal Jiaqi_Jine_TiaoxiuBz { get; set; } + public decimal Jiaqi_Jine_NianjiaBz { get; set; } + public decimal GwsrkhJineQianhege { get; set; } + public decimal GwsrkhJineQiwang { get; set; } + public decimal GwsrkhJineHege { get; set; } + public decimal GwsrkhJineShengren { get; set; } + public decimal GwsrkhJineChaoshengren { get; set; } + public decimal Jiaban_GwsrkhJineQianhege { get; set; } + public decimal Jiaban_GwsrkhJineQiwang { get; set; } + public decimal Jiaban_GwsrkhJineHege { get; set; } + public decimal Jiaban_GwsrkhJineShengren { get; set; } + public decimal Jiaban_GwsrkhJineChaoshengren { get; set; } +} + +// ==================== 薪资配置-挡车/车头岗位 ==================== + +public class ConfDcgListItemDto +{ + public long Id { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public decimal JineJisuanjishu50 { get; set; } + public decimal JineJisuanjishu60 { get; set; } + public decimal JineJisuanjishu70 { get; set; } + public decimal JineJisuanjishu100 { get; set; } + public decimal JineCtg { get; set; } +} + +public class ConfDcgSaveDto +{ + public long Id { get; set; } + public long DepId { get; set; } + public decimal JineJisuanjishu50 { get; set; } + public decimal JineJisuanjishu60 { get; set; } + public decimal JineJisuanjishu70 { get; set; } + public decimal JineJisuanjishu100 { get; set; } + public decimal JineCtg { get; set; } +} + +// ==================== 挡车纤度奖扣 ==================== + +public class DcxdjkListItemDto +{ + public long Id { get; set; } + public string GuiGe { get; set; } = ""; + public decimal XianDu { get; set; } + public decimal JiangLi { get; set; } + public decimal KouKuan { get; set; } +} + +public class DcxdjkSaveDto +{ + public long Id { get; set; } + public string GuiGe { get; set; } = ""; + public decimal XianDu { get; set; } + public decimal JiangLi { get; set; } + public decimal KouKuan { get; set; } +} + +// ==================== 薪资价格配置 ==================== + +public class XzjgListItemDto +{ + public long Id { get; set; } + public string Group { get; set; } = ""; + public string Key { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Price { get; set; } + public string Memo { get; set; } = ""; +} + +public class XzjgSaveDto +{ + public long Id { get; set; } + public string Group { get; set; } = ""; + public string Key { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Price { get; set; } + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/HsDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/HsDtos.cs new file mode 100644 index 0000000..0fe3028 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/HsDtos.cs @@ -0,0 +1,105 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 秤大丝(成筒丝 CDS) ==================== + +/// 秤大丝列表行(落丝记录 + 秤丝汇总) +public class CdsListItemDto +{ + public long LsjlId { get; set; } + public DateTime WorkDate { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; + public int Jizu { get; set; } + public int Huishu { get; set; } + public string Guige { get; set; } = ""; + public string ZhuangKouName { get; set; } = ""; + public string Yanse { get; set; } = ""; + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } = ""; + public int Chetaishu { get; set; } + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public double Hours { get; set; } + public int Tongshu { get; set; } + public int Meitongjianliang { get; set; } + public int YingchengNum { get; set; } // 应秤数 = 车台数 × 4 × (半车 ? 0.5 : 1) + public bool IsWeighed { get; set; } // 是否已秤 + public int Num { get; set; } // 已秤数 + public int Siliang { get; set; } // 已秤丝量(g) + public int Jianliang { get; set; } // 茧量(g) + public decimal Saozhe { get; set; } // 缫折 + public decimal Taichan { get; set; } // 台产 +} + +/// 秤大丝称重弹窗数据 +public class CdsEditDto +{ + public long LsjlId { get; set; } + public string DeptName { get; set; } = ""; + public int Jizu { get; set; } + public int Huishu { get; set; } + public string ZhuangKouName { get; set; } = ""; + public string Yanse { get; set; } = ""; + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } = ""; + public string Guige { get; set; } = ""; + public int ChetaiStart { get; set; } + public int ChetaiEnd { get; set; } + public int Chetaishu { get; set; } + public int YingchengNum { get; set; } + public List Chetais { get; set; } = new(); +} + +/// 秤大丝车台明细 DTO +public class CdsChetaiDto +{ + public int Chetaihao { get; set; } + public int Sl1 { get; set; } + public int Sl2 { get; set; } + public int Sl3 { get; set; } + public int Sl4 { get; set; } + public int Num { get; set; } // 个数 + public int Siliang { get; set; } // 小计丝量 +} + +/// 秤大丝保存请求 +public class CdsSaveDto +{ + public long LsjlId { get; set; } + public List Chetais { get; set; } = new(); +} + +// ==================== 公量纤度 ==================== + +/// 公量纤度列表行(落丝记录 + 纤度检验平均纤度 + 公量纤度) +public class GlxdListItemDto +{ + public long LsjlId { get; set; } + public DateTime WorkDate { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; + public int Jizu { get; set; } + public int Huishu { get; set; } + public string Guige { get; set; } = ""; + public string ZhuangKouName { get; set; } = ""; + public string Yanse { get; set; } = ""; + public bool Isbanche { get; set; } + public string IsbancheName { get; set; } = ""; + public int Chetaishu { get; set; } + public double Avgxiandu { get; set; } // 平均纤度(来自纤度检验) + public decimal Gongliangxiandu { get; set; } // 公量纤度 +} + +/// 公量纤度批量保存请求 +public class GlxdSaveDto +{ + public DateTime Date { get; set; } + public long DeptId { get; set; } + public List Items { get; set; } = new(); +} + +public class GlxdSaveItemDto +{ + public long LsjlId { get; set; } + public decimal Gongliangxiandu { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/HsTzDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/HsTzDtos.cs new file mode 100644 index 0000000..529c997 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/HsTzDtos.cs @@ -0,0 +1,129 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 丝片检查 ==================== + +/// 丝片检查列表行 +public class SpjcListItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public int Pianshu { get; set; } + public int PianshuXuegong { get; set; } + + public int QsJiahua { get; set; } + public int QsYouwu { get; set; } + public int QsHeidian { get; set; } + public int QsShangsi { get; set; } + public int QsYetiao { get; set; } + public int QsXiaosipian { get; set; } + public int QsQita { get; set; } + public int QsHeji { get; set; } + + public int HsYouwu { get; set; } + public int HsShangsi { get; set; } + public int HsShuangsi { get; set; } + public int HsZhisi { get; set; } + public int HsZhantiao { get; set; } + public int HsYingjiao { get; set; } + public int HsGuojiao { get; set; } + public int HsShouchu { get; set; } + public int HsWuran { get; set; } + public int HsLiansi { get; set; } + public int HsWdmt { get; set; } + public int HsMaoluan { get; set; } + public int HsQita { get; set; } + public int HsHeji { get; set; } + + public string Memo { get; set; } = ""; +} + +/// 丝片检查新增/修改:所有数量前端录入,合计由服务端重算 +public class SpjcSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DeptId { get; set; } + public string DeptName { get; set; } = ""; // 仅用于编辑回填显示 + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; // 仅用于编辑回填显示 + public string Guige { get; set; } = ""; + public int Pianshu { get; set; } + public int PianshuXuegong { get; set; } + + public int QsJiahua { get; set; } + public int QsYouwu { get; set; } + public int QsHeidian { get; set; } + public int QsShangsi { get; set; } + public int QsYetiao { get; set; } + public int QsXiaosipian { get; set; } + public int QsQita { get; set; } + + public int HsYouwu { get; set; } + public int HsShangsi { get; set; } + public int HsShuangsi { get; set; } + public int HsZhisi { get; set; } + public int HsZhantiao { get; set; } + public int HsYingjiao { get; set; } + public int HsGuojiao { get; set; } + public int HsShouchu { get; set; } + public int HsWuran { get; set; } + public int HsLiansi { get; set; } + public int HsWdmt { get; set; } + public int HsMaoluan { get; set; } + public int HsQita { get; set; } + + public string Memo { get; set; } = ""; +} + +// ==================== 丝片台帐 ==================== + +/// 丝片台帐列表行 +public class SptzListItemDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public DateTime DateLast { get; set; } + public int JsShangcun { get; set; } + public int JsXiasi { get; set; } + public int JsJinsi { get; set; } + public int JsSrylJrxz { get; set; } + public int JsJincun { get; set; } + public int FsFsps { get; set; } + public int BsBsps { get; set; } + public int ZlZhengpin { get; set; } + public int ZlTichu { get; set; } + public string Memo { get; set; } = ""; +} + +/// 丝片台帐新增默认值:上一日本台账 + 按当日「丝片检查」汇总出的整理片数 +public class SptzDefaultsDto +{ + public DateTime Date { get; set; } + public DateTime? DateLast { get; set; } // 上一本台账日期(无则为 null) + public int JsShangcun { get; set; } // 上存 = 上一日今存 + public DateTime? LatestDate { get; set; } // 当前最新台账日期(用于「无法新增该日期台账」校验) + public int ZlTichu { get; set; } // 剔除片数 = Σ丝片检查前缫合计 + public int ZlZhengpin { get; set; } // 正品片数 = Σ(总片数 + 学工片数) − 剔除 + public bool HasSpjc { get; set; } // 当日是否存在丝片检查数据 +} + +/// 丝片台帐保存请求 +public class SptzSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public DateTime DateLast { get; set; } + public int JsShangcun { get; set; } + public int JsXiasi { get; set; } + public int JsJinsi { get; set; } + public int FsFsps { get; set; } + public int BsBsps { get; set; } + public int ZlZhengpin { get; set; } + public int ZlTichu { get; set; } + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/KaoqinDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/KaoqinDtos.cs new file mode 100644 index 0000000..0711dbd --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/KaoqinDtos.cs @@ -0,0 +1,187 @@ +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 考勤登记(CQDJ) ==================== + +/// 登记明细(读 / 写共用) +public class CqdjItemDto +{ + public long Id { get; set; } + public long DepId { get; set; } // 出勤部门(流动 / 代班时可为其它部门) + public long WorktypeId { get; set; } // 出勤工种 + public int TypeCq { get; set; } // 出勤类型(1正常 2请假 3流动 4加班 5代班 6外派 7旷工 8工伤) + public int TypeQj { get; set; } // 请假类型(0无 1事假 2病假 3产假 4婚假 5丧假 6公假 7年假 9调休 10轮休) + public decimal Hours { get; set; } + public string Jizu { get; set; } = ""; + public string CtStart { get; set; } = ""; + public string CtEnd { get; set; } = ""; + public int DangcheNum { get; set; } + public string Memo { get; set; } = ""; +} + +/// 登记行(一人一天) +public class CqdjRowDto +{ + public long Id { get; set; } // 已保存的主表 Id(0=未保存) + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public int Banci { get; set; } + public bool IsXuegong { get; set; } // 学工(未转正) + public bool Saved { get; set; } // 是否已入库 + + public decimal HoursStandard { get; set; } + public decimal HoursCq { get; set; } + public decimal HoursQj { get; set; } + public decimal HoursLd { get; set; } + public decimal HoursJb { get; set; } + public decimal HoursDb { get; set; } + public decimal HoursWp { get; set; } + public decimal HoursKg { get; set; } + public decimal HoursGs { get; set; } + public int CsYeban { get; set; } + public string Memo { get; set; } = ""; + + public List Items { get; set; } = new(); +} + +/// 登记查询结果(一次性返回整组,前端整组编辑) +public class CqdjViewDto +{ + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public long DepId { get; set; } + public long WorktypeId { get; set; } + public int Banci { get; set; } + public List Rows { get; set; } = new(); +} + +/// 整组保存入参(先软删该 日期+部门+工种+班次 的旧登记,再整批新增) +public class CqdjSaveDto +{ + public DateTime Date { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } // 0 = 全部工种 + public int Banci { get; set; } + public List Rows { get; set; } = new(); +} + +public class CqdjSaveRowDto +{ + public long EmployeeId { get; set; } + public long DepId { get; set; } // 0 = 取职工所在部门 + public long WorktypeId { get; set; } // 0 = 取职工工种 + public bool IsXuegong { get; set; } + public decimal HoursStandard { get; set; } + public int CsYeban { get; set; } + public string Memo { get; set; } = ""; + public List Items { get; set; } = new(); +} + +/// 「添加职工」下拉选项 +public class CqdjEmpOptionDto +{ + public long Id { get; set; } + public string Code { get; set; } = ""; + public string Name { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public bool IsXuegong { get; set; } +} + +// ==================== 考勤统计(CQTJ) ==================== + +/// 统计行(一人一行,月快照) +public class CqtjRowDto +{ + public long Id { get; set; } + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long WorktypeId { get; set; } + public string WorktypeName { get; set; } = ""; + public long EmployeeId { get; set; } + public string EmployeeCode { get; set; } = ""; + public string EmployeeName { get; set; } = ""; + + public decimal DaysCqStandard { get; set; } + public decimal DaysCqActual { get; set; } + public decimal CqTianci { get; set; } + + public decimal HoursCq { get; set; } + public decimal HoursQj { get; set; } + public decimal HoursLd { get; set; } + public decimal HoursJb { get; set; } + public decimal HoursDb { get; set; } + public decimal HoursWp { get; set; } + public decimal HoursKg { get; set; } + public int CsYeban { get; set; } + + public decimal HoursQjShijia { get; set; } + public decimal HoursQjBj { get; set; } + public decimal HoursQjHj { get; set; } + public decimal HoursQjCj { get; set; } + public decimal HoursQjLx { get; set; } + public decimal HoursQjSangjia { get; set; } + public decimal HoursQjGj { get; set; } + public decimal HoursQjNj { get; set; } + public decimal HoursQjTx { get; set; } + + public decimal HoursXgCq { get; set; } + public decimal HoursXgJiaban { get; set; } + + public decimal HoursFadingjiaqi { get; set; } + public decimal HoursQjQqmc { get; set; } + public decimal HoursQjQqcal { get; set; } + + public decimal JineQuanqin { get; set; } + public decimal JineJb { get; set; } + public decimal JineLiudong { get; set; } + public decimal JineDaiban { get; set; } + public decimal JineYb { get; set; } + public decimal JineKgKk { get; set; } + + public decimal JineJqBzHj { get; set; } + public decimal JineJqBzBj { get; set; } + public decimal JineJqBzLx { get; set; } + public decimal JineJqBzSangjia { get; set; } + public decimal JineJqBzFading { get; set; } + public decimal JineJqBzTxnj { get; set; } + public decimal JineJqBz { get; set; } + + public string Memo { get; set; } = ""; +} + +/// 统计整组保存(先软删该 年/月/部门 的旧快照,再整批新增) +public class CqtjSaveDto +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public List Rows { get; set; } = new(); +} + +/// 一键生成入参 +public class CqtjGenerateDto +{ + public int Year { get; set; } + public int Month { get; set; } +} + +/// 统计录入视图(附加展示用的账期信息) +public class CqtjViewDto +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public decimal QsQuanqinDays { get; set; } // 薪资配置账期的前缫出勤天数 + public bool Exists { get; set; } // true=库中已有快照,false=本次实时算出 + public List Rows { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs index 76dbc2a..aa9e3e8 100644 --- a/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs +++ b/backend/F9WebAiTest.Application/Dtos/Prod/LsjlDtos.cs @@ -18,6 +18,8 @@ public class LsjlDto public bool Isbanche { get; set; } public string IsbancheName { get; set; } public double Hours { get; set; } + public int Tongshu { get; set; } + public int Meitongjianliang { get; set; } public string Guige { get; set; } public double Chesu { get; set; } } @@ -36,6 +38,8 @@ public class LsjlCreateDto public int Chetaishu { get; set; } public bool Isbanche { get; set; } public double Hours { get; set; } + public int Tongshu { get; set; } + public int Meitongjianliang { get; set; } public string Guige { get; set; } = ""; public double Chesu { get; set; } } diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/XjrbDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/XjrbDtos.cs new file mode 100644 index 0000000..c04abbb --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/XjrbDtos.cs @@ -0,0 +1,228 @@ +using F9WebAiTest.Domain.Entities.Prod; + +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 选茧日报 ==================== + +public class XjrbListDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + + public int YlLrBaoshu { get; set; } + public decimal YlLrZhongliang { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int LjjBaoshu { get; set; } + public decimal LjjZhongliang { get; set; } + public int MjBaoshu { get; set; } + public decimal MjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public decimal TotalZhongliang { get; set; } + public int ThBaoshu { get; set; } + public decimal ThZhongliang { get; set; } + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } +} + +public class XjrbSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long GyzkId { get; set; } + + public int YlLrBaoshu { get; set; } + public decimal YlLrZhongliang { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int LjjBaoshu { get; set; } + public decimal LjjZhongliang { get; set; } + public int MjBaoshu { get; set; } + public decimal MjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public int ThBaoshu { get; set; } + public decimal ThZhongliang { get; set; } + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } +} + +/// 盈亏 / 联统率计算结果(对齐 F9 GetXjrbCountYingkuiQuery 返回字典) +public class XjrbYingkuiDto +{ + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } + public DateTime StartDate { get; set; } + public string StartDateText => StartDate.ToString("yyyy-MM-dd"); +} + +// ==================== 选茧计划 ==================== + +public class XjrbPlanListDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long YllingliaodanId { get; set; } + public string YllingliaodanCode { get; set; } = ""; + + public int YlBaoshu { get; set; } + public decimal YlZhongliang { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public decimal TotalZhongliang { get; set; } + public decimal LiantongRate { get; set; } + public decimal Yingkui { get; set; } + public bool IsFinish { get; set; } +} + +public class XjrbPlanSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long GyzkId { get; set; } + public long YllingliaodanId { get; set; } + + public int YlBaoshu { get; set; } + public decimal YlZhongliang { get; set; } + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } +} + +// ==================== 选茧计划明细 ==================== + +public class XjrbItemDto +{ + public long Id { get; set; } + public long XjrbPlanId { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public decimal TotalZhongliang { get; set; } + public bool IsFinish { get; set; } +} + +public class XjrbItemSaveDto +{ + public long Id { get; set; } + public long XjrbPlanId { get; set; } + public DateTime Date { get; set; } + + public int LtjBaoshu { get; set; } + public decimal LtjZhongliang { get; set; } + public int HwjjBaoshu { get; set; } + public decimal HwjjZhongliang { get; set; } + public int WjjBaoshu { get; set; } + public decimal WjjZhongliang { get; set; } + public int SgjBaoshu { get; set; } + public decimal SgjZhongliang { get; set; } + public int XjjBaoshu { get; set; } + public decimal XjjZhongliang { get; set; } + public int JyBaoshu { get; set; } + public decimal JyZhongliang { get; set; } + public int JhBaoshu { get; set; } + public decimal JhZhongliang { get; set; } + + public bool IsFinish { get; set; } +} + +// ==================== 选茧班组 ==================== + +public class XjpbBanzuDto +{ + public long Id { get; set; } + public string Banci { get; set; } = ""; + public int Zu { get; set; } +} + +public class XjpbBanzuZhigongDto +{ + public long Id { get; set; } + public long BanzuId { get; set; } + public string Banci { get; set; } = ""; + public int Zu { get; set; } + public long YgId { get; set; } + public string YgName { get; set; } = ""; + public string YgPhone { get; set; } = ""; + public int Sort { get; set; } +} + +/// 班组「编辑录入」保存项:职工 + 是否选中 + 组别(对齐 F9 XjpbBanzuZhigongCombinedDto) +public class XjpbBanzuZhigongSaveDto +{ + public long Id { get; set; } + public string Banci { get; set; } = ""; + public int Zu { get; set; } + public long YgId { get; set; } + public string YgName { get; set; } = ""; + public string YgPhone { get; set; } = ""; + public int Sort { get; set; } + public bool Selected { get; set; } +} diff --git a/backend/F9WebAiTest.Application/Dtos/Prod/ZjrbDtos.cs b/backend/F9WebAiTest.Application/Dtos/Prod/ZjrbDtos.cs new file mode 100644 index 0000000..9fcc04b --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Prod/ZjrbDtos.cs @@ -0,0 +1,111 @@ +using F9WebAiTest.Domain.Enums.SYS.Employees; + +namespace F9WebAiTest.Application.Dtos.Prod; + +// ==================== 煮茧日报 ==================== + +/// 煮茧日报列表行(主表 + 机组桶数合计) +public class ZjrbReadDto_View +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public DateTime? LastDate { get; set; } + public int StandardLuositongshu { get; set; } + public int MeitongJianliang { get; set; } + + public int BaoshuShangcun { get; set; } + public int BaoshuLingru { get; set; } + public int BaoshuSaosi { get; set; } + public int BaoshuTuihui { get; set; } + public int BaoshuJincun { get; set; } + + public decimal JianliangShangcun { get; set; } + public decimal JianliangLingru { get; set; } + public decimal JianliangSaosi { get; set; } + public decimal JianliangTuihui { get; set; } + public decimal JianliangJincun { get; set; } + + // 机组桶数合计(列表下方明细/列展示用) + public int TongshuShangcun { get; set; } + public int TongshuJinsao { get; set; } + public int TongshuJinluo { get; set; } + public int TongshuJincun { get; set; } + public decimal JizuSaosiHours { get; set; } +} + +/// 煮茧日报-机组成员 +public class ZjrbJizuDto +{ + public long Id { get; set; } + public long ZjrbId { get; set; } + public int Jizu { get; set; } + public decimal JizuSaosiHours { get; set; } + public int TongshuShangcun { get; set; } + /// 甲班今缫桶数(存于机组班次表) + public int TongshuJinsaoJiaban { get; set; } + /// 乙班今缫桶数(存于机组班次表) + public int TongshuJinsaoYiban { get; set; } + /// 今缫桶数 = 甲班 + 乙班(只读合计) + public int TongshuJinsao { get; set; } + public int TongshuJinluo { get; set; } + public int TongshuJincun { get; set; } +} + +/// 煮茧日报-班次成员(包数 / 重量) +public class ZjrbBanciDto +{ + public long Id { get; set; } + public long ZjrbId { get; set; } + public Prod_Banci_Enum Banci { get; set; } + public string BanciName { get; set; } = ""; + + public int BaoshuShangcun { get; set; } + public int BaoshuLingru { get; set; } + public int BaoshuSaosi { get; set; } + public int BaoshuTuihui { get; set; } + public int BaoshuJincun { get; set; } + + public decimal JianliangShangcun { get; set; } + public decimal JianliangLingru { get; set; } + public decimal JianliangSaosi { get; set; } + public decimal JianliangTuihui { get; set; } + public decimal JianliangJincun { get; set; } +} + +/// 煮茧日报整条保存(主表 + 班次 + 机组 + 机组班次) +public class ZjrbSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public string Guige { get; set; } = ""; + public DateTime? LastDate { get; set; } + public int StandardLuositongshu { get; set; } + public int MeitongJianliang { get; set; } + + /// 联统茧标重(kg/包),来自参数配置「生产\前缫\煮茧 → 联统茧标重」,未配置为 0 + public decimal LiantongBiaozhong { get; set; } + + public int BaoshuShangcun { get; set; } + public int BaoshuLingru { get; set; } + public int BaoshuSaosi { get; set; } + public int BaoshuTuihui { get; set; } + public int BaoshuJincun { get; set; } + + public decimal JianliangShangcun { get; set; } + public decimal JianliangLingru { get; set; } + public decimal JianliangSaosi { get; set; } + public decimal JianliangTuihui { get; set; } + public decimal JianliangJincun { get; set; } + + public List Bancis { get; set; } = new(); + public List Jizus { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs b/backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs new file mode 100644 index 0000000..d404c07 --- /dev/null +++ b/backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs @@ -0,0 +1,695 @@ +using F9WebAiTest.Domain.Entities.Wms; + +namespace F9WebAiTest.Application.Dtos.Wms; + +/// 下拉选项(单据类型 / 台账项目 / 往来单位 / 部门 / 庄口 / 物资) +public class WmsOptionDto +{ + public long Id { get; set; } + public string Name { get; set; } = ""; + public string Code { get; set; } = ""; + public string Ext { get; set; } = ""; +} + +public class WmsTypeOptionDto +{ + public int Value { get; set; } + public string Name { get; set; } = ""; +} + +public class WmsOptionsDto +{ + public List RukuTypes { get; set; } = new(); + public List ChukuTypes { get; set; } = new(); + public List Wuzi { get; set; } = new(); + public List YuanliaoZhuangkou { get; set; } = new(); + public List CraftZhuangkou { get; set; } = new(); + public List Departments { get; set; } = new(); + public List Gongyingshangs { get; set; } = new(); +} + +// ==================== 入库 ==================== + +public class WmsRukuListDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public int Type { get; set; } + public string TypeName { get; set; } = ""; + public string Code { get; set; } = ""; + public long GysId { get; set; } + public string GysName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public string Lianxiren { get; set; } = ""; + public string Phone { get; set; } = ""; + public string Carno { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string Caigouyuan { get; set; } = ""; + public string PayStatus { get; set; } = ""; + public int RkStatus { get; set; } + public string RkStatusName { get; set; } = ""; + public int BxStatus { get; set; } + public string BxStatusName { get; set; } = ""; + public string Description { get; set; } = ""; + public int Jianshu { get; set; } + public int BaojianNum { get; set; } + public decimal Jingzhong { get; set; } + public decimal Shuliang { get; set; } + public decimal Jine { get; set; } + public int DetailCount { get; set; } +} + +public class WmsRukuDetailDto +{ + public long Id { get; set; } + public long RukuId { get; set; } + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal Sunhao { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public int BaojianNum { get; set; } + public string PiciIds { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +public class WmsBangmadanDto +{ + public long Id { get; set; } + public long DetailId { get; set; } + public bool IsChuku { get; set; } + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public DateTime? GuobangShijian { get; set; } +} + +public class WmsRukuEditDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long GysId { get; set; } + public string GysName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public string Lianxiren { get; set; } = ""; + public string Phone { get; set; } = ""; + public string Carno { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string Caigouyuan { get; set; } = ""; + public string PayStatus { get; set; } = ""; + public int RkStatus { get; set; } + public int BxStatus { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsRukuSaveDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long GysId { get; set; } + public long KhId { get; set; } + public long GyzkId { get; set; } + public long DepId { get; set; } + public string Lianxiren { get; set; } = ""; + public string Phone { get; set; } = ""; + public string Carno { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string Caigouyuan { get; set; } = ""; + public string PayStatus { get; set; } = ""; + public int RkStatus { get; set; } + public int BxStatus { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsRukuDetailSaveDto +{ + public long Id { get; set; } + public long WzmlId { get; set; } + public long YlzkId { get; set; } + public long GyzkId { get; set; } + public long DepId { get; set; } + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal Sunhao { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public int BaojianNum { get; set; } + public string PiciIds { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +// ==================== 出库 ==================== + +public class WmsChukuListDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public int Type { get; set; } + public string TypeName { get; set; } = ""; + public string Code { get; set; } = ""; + public long WldwId { get; set; } + public string WldwName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long LingliaoDepId { get; set; } + public string LingliaoDepName { get; set; } = ""; + public long UseDepId { get; set; } + public string UseDepName { get; set; } = ""; + public long DbdepId { get; set; } + public string DbdepName { get; set; } = ""; + public string Lingliaoren { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string UseLocation { get; set; } = ""; + public int CkStatus { get; set; } + public string CkStatusName { get; set; } = ""; + public bool IsHongchong { get; set; } + public string Description { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public decimal Shuliang { get; set; } + public decimal Jine { get; set; } + public int DetailCount { get; set; } +} + +public class WmsChukuDetailDto +{ + public long Id { get; set; } + public long ChukuId { get; set; } + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal KucunNum { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public string PcAndBjIds { get; set; } = ""; + public string Lingyongren { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +public class WmsChukuEditDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long WldwId { get; set; } + public string WldwName { get; set; } = ""; + public long KhId { get; set; } + public string KhName { get; set; } = ""; + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long LingliaoDepId { get; set; } + public string LingliaoDepName { get; set; } = ""; + public long UseDepId { get; set; } + public string UseDepName { get; set; } = ""; + public long DbdepId { get; set; } + public string DbdepName { get; set; } = ""; + public string Lingliaoren { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string UseLocation { get; set; } = ""; + public int CkStatus { get; set; } + public bool IsHongchong { get; set; } + public long HongchongId { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsChukuSaveDto +{ + public long Id { get; set; } + public int Module { get; set; } + public DateTime Date { get; set; } + public int Type { get; set; } + public string Code { get; set; } = ""; + public long WldwId { get; set; } + public long KhId { get; set; } + public long GyzkId { get; set; } + public long LingliaoDepId { get; set; } + public long UseDepId { get; set; } + public long DbdepId { get; set; } + public string Lingliaoren { get; set; } = ""; + public string Kuguan { get; set; } = ""; + public string UseLocation { get; set; } = ""; + public int CkStatus { get; set; } + public string Description { get; set; } = ""; + public List Details { get; set; } = new(); +} + +public class WmsChukuDetailSaveDto +{ + public long Id { get; set; } + public long WzmlId { get; set; } + public long YlzkId { get; set; } + public long GyzkId { get; set; } + public long DepId { get; set; } + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal KucunNum { get; set; } + public string PiciCodes { get; set; } = ""; + public int PiciNum { get; set; } + public string PcAndBjIds { get; set; } = ""; + public string Lingyongren { get; set; } = ""; + public string Banci { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public List Bangmadans { get; set; } = new(); +} + +// ==================== 库存台账 ==================== + +public class WmsTzDateDto +{ + public long Id { get; set; } + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public DateTime LastDate { get; set; } + public string LastDateText => LastDate.ToString("yyyy-MM-dd"); + public int ItemCount { get; set; } +} + +public class WmsTzItemDto +{ + public long Id { get; set; } + public long TzDateId { get; set; } + public long ItemId { get; set; } + public string SubKey { get; set; } = ""; + public string ItemName { get; set; } = ""; + public string ItemCode { get; set; } = ""; + public decimal Ydayjianshu { get; set; } + public decimal Ydayjingzhong { get; set; } + public decimal Tdayrukujianshu { get; set; } + public decimal Tdayrukujingzhong { get; set; } + public decimal Tdaychukujianshu { get; set; } + public decimal Tdaychukujingzhong { get; set; } + public decimal Yingkui { get; set; } + public decimal Tdayjianshu { get; set; } + public decimal Tdayjingzhong { get; set; } + public decimal Yangjianjianshu { get; set; } + public bool AllChuku { get; set; } + public string Description { get; set; } = ""; +} + +public class WmsTzEditDto +{ + public long TzDateId { get; set; } + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } + public DateTime LastDate { get; set; } + public List Items { get; set; } = new(); +} + +public class WmsTzSaveDto +{ + public long TzDateId { get; set; } + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } + public List Items { get; set; } = new(); +} + +public class WmsTzGenerateDto +{ + public int Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } +} + +// ==================== 库存 / 流水 ==================== + +public class WmsKucunDto +{ + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public string Guige { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } +} + +public class WmsCrkDto +{ + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public bool IsRuku { get; set; } + public string Kind => IsRuku ? "入库" : "出库"; + public string Code { get; set; } = ""; + public string TypeName { get; set; } = ""; + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +// ==================== 生丝批次 / 包件 ==================== + +public class WmsPiciListDto +{ + public long Id { get; set; } + public string Code { get; set; } = ""; + public int CodeType { get; set; } + public int Year { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public DateTime? ScDate { get; set; } + public long GyzkId { get; set; } + public string GyzkName { get; set; } = ""; + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public string Guige { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public string YoujiPutong { get; set; } = ""; + public int Baoshu { get; set; } + public int StartBaojianNum { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } + public int KcStatus { get; set; } + public string KcStatusName { get; set; } = ""; + public long RkItemId { get; set; } + public DateTime? RkDate { get; set; } + public DateTime CkLastDate { get; set; } + public int CkBaoshu { get; set; } + public decimal CkJingzhong { get; set; } + public int KcBaoshu { get; set; } + public string Dengji { get; set; } = ""; + public decimal Qingjie { get; set; } + public decimal Jiejing { get; set; } + public decimal Piancha { get; set; } + public decimal Zongcha { get; set; } + public decimal Qieduan { get; set; } + public string Description { get; set; } = ""; + public int BaojianNum { get; set; } +} + +public class WmsPiciSaveDto +{ + public long Id { get; set; } + public string Code { get; set; } = ""; + public int CodeType { get; set; } + public DateTime Date { get; set; } + public DateTime? ScDate { get; set; } + public long GyzkId { get; set; } + public long WzmlId { get; set; } + public long DepId { get; set; } + public string Guige { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public string YoujiPutong { get; set; } = ""; + public int Baoshu { get; set; } + public int StartBaojianNum { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } + public int KcStatus { get; set; } + public string Dengji { get; set; } = ""; + public decimal Qingjie { get; set; } + public decimal Jiejing { get; set; } + public decimal Piancha { get; set; } + public decimal Zongcha { get; set; } + public decimal Qieduan { get; set; } + public string Description { get; set; } = ""; + public List Baojians { get; set; } = new(); +} + +public class WmsPiciBaojianDto +{ + public long Id { get; set; } + public long PiciId { get; set; } + public int Baojianhao { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } + public decimal Huichaolv { get; set; } + public bool CkIsck { get; set; } + public long CkItemId { get; set; } + public DateTime? CkDate { get; set; } + public decimal CkJingzhong { get; set; } + public string Description { get; set; } = ""; +} + +/// 生丝出库选批次(批次 + 未出库包件),对齐 F9 SSPici_GetChukuSelectAsync +public class WmsPiciChukuSelectDto +{ + public long PiciId { get; set; } + public string PiciCode { get; set; } = ""; + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Guige { get; set; } = ""; + public string WzmlName { get; set; } = ""; + public string GyzkName { get; set; } = ""; + public string DepName { get; set; } = ""; + public string JidiWaigou { get; set; } = ""; + public int Baoshu { get; set; } // 组批包数 + public int KcBaoshu { get; set; } // 库存(未出库)包数 + public decimal Jingzhong { get; set; } // 组批净重 + public decimal KcJingzhong { get; set; } // 库存净重 + public string Description { get; set; } = ""; + public List Baojians { get; set; } = new(); +} + +/// 生丝批次-质检 +public class WmsPiciQcDto +{ + public long Id { get; set; } + public long PiciId { get; set; } + public string Pingwei { get; set; } = ""; + public string Seze { get; set; } = ""; + public decimal AvgHuichaolv { get; set; } + public decimal AvgGongliangXiandu { get; set; } + public decimal XianduPiancha { get; set; } + public decimal XianduZdpiancha { get; set; } + public decimal Qingjie { get; set; } + public decimal Jiejing { get; set; } + public int JunyunYd { get; set; } + public int JunyunEd { get; set; } + public int JunyunSd { get; set; } + public int Qieduan { get; set; } + public string DuanlieQiangdu { get; set; } = ""; + public string DuanlieShenchanglv { get; set; } = ""; + public int Baohe { get; set; } + public string WgWg { get; set; } = ""; + public string WgYanse { get; set; } = ""; + public string WgGuangze { get; set; } = ""; + public string WgShougan { get; set; } = ""; + public int? QjZhuyao { get; set; } + public int? QjCiyaoFeisi { get; set; } + public int? QjCiyaoDacao { get; set; } + public int? QjCiyaoNianfucao { get; set; } + public int? QjCiyaoDachangjie { get; set; } + public int? QjCiyaoZhongluoxuan { get; set; } + public int? QjPutongXiaocao { get; set; } + public int? QjPutongChangjie { get; set; } + public int? QjPutongLuoxuan { get; set; } + public int? QjPutongHuanjie { get; set; } + public int? Jj100 { get; set; } + public int? Jj95 { get; set; } + public int? Jj90 { get; set; } + public int? Jj85 { get; set; } + public int? Jj80 { get; set; } + public int? Jj75 { get; set; } + public int? Jj70 { get; set; } + public int? Jj65 { get; set; } + public int? Jj60 { get; set; } + public int? Jj55 { get; set; } + public int? Jj50 { get; set; } + public int? Jj45 { get; set; } + public string JjYuanyin { get; set; } = ""; + public string Description { get; set; } = ""; +} + +/// 生丝批次-纤度检验 +public class WmsPiciQcXdjyDto +{ + public long Id { get; set; } + public long PiciId { get; set; } + public decimal Xiandu { get; set; } + public int Num { get; set; } +} + +// ==================== 原料采购 / 翻包计划 ==================== + +public class WmsCaigouDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Code { get; set; } = ""; + public long GysId { get; set; } + public string GysName { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +public class WmsFbplanDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Code { get; set; } = ""; + public long YlzkId { get; set; } + public string YlzkName { get; set; } = ""; + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public int FbType { get; set; } + public string FbTypeName { get; set; } = ""; + public int RukuStatus { get; set; } + public string RukuStatusName { get; set; } = ""; + public string Description { get; set; } = ""; + public decimal YiwanchengJianshu { get; set; } + public decimal YiwanchengJingzhong { get; set; } + public List Records { get; set; } = new(); +} + +public class WmsFbjlDto +{ + public long Id { get; set; } + public long FbplanId { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public string Description { get; set; } = ""; +} + +// ==================== 物资采购计划 ==================== + +public class WmsCgplanListDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string DateText => Date.ToString("yyyy-MM-dd"); + public string Code { get; set; } = ""; + public long DepId { get; set; } + public string DepName { get; set; } = ""; + public int Status { get; set; } + public string StatusName { get; set; } = ""; + public string Description { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Jine { get; set; } + public int ItemCount { get; set; } +} + +public class WmsCgplanItemDto +{ + public long Id { get; set; } + public long CgplanId { get; set; } + public long WzmlId { get; set; } + public string WzmlName { get; set; } = ""; + public string WzmlCode { get; set; } = ""; + public string Unit { get; set; } = ""; + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +public class WmsCgplanSaveDto +{ + public long Id { get; set; } + public DateTime Date { get; set; } + public string Code { get; set; } = ""; + public long DepId { get; set; } + public int Status { get; set; } + public string Description { get; set; } = ""; + public List Items { get; set; } = new(); +} diff --git a/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs b/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs deleted file mode 100644 index 4aa3bb1..0000000 --- a/backend/F9WebAiTest.Application/Services/Attendance/AttendanceService.cs +++ /dev/null @@ -1,573 +0,0 @@ -using System.Text.Json; -using F9WebAiTest.Application.Common; -using F9WebAiTest.Application.Dtos; -using F9WebAiTest.Application.Dtos.Attendance; -using F9WebAiTest.Application.Services.Notice; -using F9WebAiTest.Domain.Common; -using F9WebAiTest.Domain.Entities; -using F9WebAiTest.Domain.Entities.Attendance; -using F9WebAiTest.Domain.Entities.Notice; -using RBAC = F9WebAiTest.Domain.Entities.SYS.RBAC; -using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; -using MediatR; - -namespace F9WebAiTest.Application.Services.Attendance; - -public class AttendanceService : IAttendanceService -{ - private readonly IRepository _attRepo; - private readonly IRepository _leaveRepo; - private readonly IRepository _approverRepo; - private readonly IRepository _empRepo; - private readonly IRepository _deptRepo; - private readonly IRepository _roleRepo; - private readonly IRepository _empRoleRepo; - private readonly IRepository _msgRepo; - private readonly IRepository _receiverRepo; - private readonly IRepository _todoRepo; - private readonly IRepository _typeRepo; - private readonly IMediator _mediator; - - // 上下班时间(可配置,这里简化固定) - private static readonly TimeSpan WorkStart = new(9, 0, 0); - private static readonly TimeSpan WorkEnd = new(18, 0, 0); - - public AttendanceService( - IRepository attRepo, - IRepository leaveRepo, - IRepository approverRepo, - IRepository empRepo, - IRepository deptRepo, - IRepository roleRepo, - IRepository empRoleRepo, - IRepository msgRepo, - IRepository receiverRepo, - IRepository todoRepo, - IRepository typeRepo, - IMediator mediator) - { - _attRepo = attRepo; - _leaveRepo = leaveRepo; - _approverRepo = approverRepo; - _empRepo = empRepo; - _deptRepo = deptRepo; - _roleRepo = roleRepo; - _empRoleRepo = empRoleRepo; - _msgRepo = msgRepo; - _receiverRepo = receiverRepo; - _todoRepo = todoRepo; - _typeRepo = typeRepo; - _mediator = mediator; - } - - public async Task PunchAsync(long employeeId) - { - var now = DateTime.Now; - var today = now.Date; - - var record = await _attRepo.FirstOrDefaultAsync(a => a.EmployeeId == employeeId && a.WorkDate == today); - - // 签到 - if (record == null) - { - var status = now.TimeOfDay <= WorkStart.Add(TimeSpan.FromMinutes(10)) ? "正常" : "迟到"; - var newRecord = new AttendanceRecord - { - EmployeeId = employeeId, - WorkDate = today, - SignInTime = now, - SignInStatus = status, - SignOutStatus = "未签退" - }; - await _attRepo.AddAsync(newRecord); - await _attRepo.SaveChangesAsync(); - - return new PunchResultDto - { - Type = "signIn", - Time = now, - Status = status, - Message = status == "正常" ? "签到成功,辛苦了!" : "签到成功(已迟到)" - }; - } - - // 已签到,进行签退 - if (record.SignOutTime == null) - { - var status = now.TimeOfDay >= WorkEnd ? "正常" : "早退"; - record.SignOutTime = now; - record.SignOutStatus = status; - await _attRepo.UpdateAsync(record); - await _attRepo.SaveChangesAsync(); - - return new PunchResultDto - { - Type = "signOut", - Time = now, - Status = status, - Message = status == "正常" ? "签退成功,明天见!" : "签退成功(早退)" - }; - } - - // 已签退 - return new PunchResultDto - { - Type = "done", - Time = now, - Status = "已完成", - Message = "今日已签到和签退,无需重复打卡" - }; - } - - public async Task> GetMyRecordsAsync(long employeeId, int pageIndex, int pageSize) - { - var records = (await _attRepo.FindAsync(a => a.EmployeeId == employeeId)) - .OrderByDescending(a => a.WorkDate) - .ToList(); - var total = records.Count; - var items = records.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); - - var emp = await _empRepo.GetByIdAsync(employeeId); - var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; - - return new PageResult - { - Items = items.Select(a => new AttendanceDto - { - Id = a.Id, - EmployeeId = a.EmployeeId, - EmployeeName = emp?.UserName ?? "", - DepartmentName = dept?.Name ?? "", - WorkDate = a.WorkDate, - SignInTime = a.SignInTime, - SignOutTime = a.SignOutTime, - SignInStatus = a.SignInStatus, - SignOutStatus = a.SignOutStatus, - Remark = a.Remark - }).ToList(), - Total = total, - PageIndex = pageIndex, - PageSize = pageSize - }; - } - - public async Task GetMySummaryAsync(long employeeId, string month) - { - // month 格式 yyyy-MM,默认当前月 - if (string.IsNullOrWhiteSpace(month)) - { - month = DateTime.Now.ToString("yyyy-MM"); - } - var start = DateTime.Parse(month + "-01"); - var end = start.AddMonths(1); - - var records = await _attRepo.FindAsync(a => - a.EmployeeId == employeeId && a.WorkDate >= start && a.WorkDate < end); - - var summary = new AttendanceSummaryDto(); - foreach (var r in records) - { - summary.TotalDays++; - if (r.SignInStatus == "迟到") summary.LateDays++; - if (r.SignOutStatus == "早退") summary.EarlyLeaveDays++; - if (r.SignInTime == null) summary.AbsentDays++; - } - summary.NormalDays = summary.TotalDays - summary.LateDays - summary.EarlyLeaveDays - summary.AbsentDays; - - // 请假天数 - var leaves = await _leaveRepo.FindAsync(l => - l.EmployeeId == employeeId && l.Status == "已批准" && l.StartTime < end && l.EndTime >= start); - summary.LeaveDays = leaves.Sum(l => (l.EndTime - l.StartTime).Days + 1); - - return summary; - } - - public async Task> GetAllRecordsAsync(AttendanceQueryDto query) - { - var records = await _attRepo.GetAllAsync(); - var filtered = records.AsQueryable(); - - if (!string.IsNullOrWhiteSpace(query.Date)) - { - var date = DateTime.Parse(query.Date).Date; - filtered = filtered.Where(a => a.WorkDate == date); - } - - var emps = await _empRepo.GetAllAsync(); - var depts = await _deptRepo.GetAllAsync(); - var empDict = emps.ToDictionary(e => e.Id, e => e); - var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); - - if (query.DepartmentId.HasValue) - { - var deptEmpIds = emps.Where(e => e.DepartmentId == query.DepartmentId.Value).Select(e => e.Id).ToHashSet(); - filtered = filtered.Where(a => deptEmpIds.Contains(a.EmployeeId)); - } - if (!string.IsNullOrWhiteSpace(query.Keyword)) - { - var kwEmpIds = emps.Where(e => e.UserName.Contains(query.Keyword) || e.LoginName.Contains(query.Keyword)) - .Select(e => e.Id).ToHashSet(); - filtered = filtered.Where(a => kwEmpIds.Contains(a.EmployeeId)); - } - - var total = filtered.LongCount(); - var items = filtered.OrderByDescending(a => a.WorkDate).ThenByDescending(a => a.SignInTime) - .Skip((query.PageIndex - 1) * query.PageSize).Take(query.PageSize).ToList(); - - return new PageResult - { - Items = items.Select(a => - { - var emp = empDict.GetValueOrDefault(a.EmployeeId); - return new AttendanceDto - { - Id = a.Id, - EmployeeId = a.EmployeeId, - EmployeeName = emp?.UserName ?? "", - DepartmentName = emp != null ? deptDict.GetValueOrDefault(emp.DepartmentId, "") : "", - WorkDate = a.WorkDate, - SignInTime = a.SignInTime, - SignOutTime = a.SignOutTime, - SignInStatus = a.SignInStatus, - SignOutStatus = a.SignOutStatus, - Remark = a.Remark - }; - }).ToList(), - Total = total, - PageIndex = query.PageIndex, - PageSize = query.PageSize - }; - } - - public async Task ApplyLeaveAsync(long employeeId, CreateLeaveDto dto) - { - if (dto.EndTime < dto.StartTime) - throw new ServiceException("结束时间不能早于开始时间"); - - // 确定审批人:优先使用前端选择的审批人,为空则回退到管理员 - var approverIds = (dto.ApproverIds ?? new List()).Where(id => id > 0).Distinct().ToList(); - if (approverIds.Count == 0) - approverIds = await GetApproverIdsAsync(); - if (approverIds.Count == 0) - throw new ServiceException("没有可用的审批人,请联系管理员"); - - var firstApprover = await _empRepo.GetByIdAsync(approverIds[0]); - var leave = new LeaveRequest - { - EmployeeId = employeeId, - Type = dto.Type, - StartTime = dto.StartTime, - EndTime = dto.EndTime, - Reason = dto.Reason ?? "", - CurrentOrderNo = 1, - ApproverId = approverIds[0], - ApproverName = firstApprover?.UserName ?? "" - }; - await _leaveRepo.AddAsync(leave); - await _leaveRepo.SaveChangesAsync(); - - // 创建审批节点(序号 = 审批顺序) - for (var i = 0; i < approverIds.Count; i++) - { - var emp = await _empRepo.GetByIdAsync(approverIds[i]); - await _approverRepo.AddAsync(new LeaveApprover - { - LeaveId = leave.Id, - ApproverId = approverIds[i], - ApproverName = emp?.UserName ?? "", - OrderNo = i + 1 - }); - } - await _approverRepo.SaveChangesAsync(); - - // 只给第一个审批人发待办(顺序审批,后续审批人待前一节点通过后才收到) - await SendLeaveApprovalAsync(leave, approverIds[0], 1, approverIds.Count); - - // 业务单据已创建,推送事件让各端刷新(多端同步) - await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leave.Id.ToString(), "Created")); - - return await ToLeaveDto(leave); - } - - public async Task> GetMyLeavesAsync(long employeeId) - { - var leaves = await _leaveRepo.FindAsync(l => l.EmployeeId == employeeId); - var result = new List(); - foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) - { - result.Add(await ToLeaveDto(l)); - } - return result; - } - - public async Task> GetAllLeavesAsync(string status) - { - var leaves = await _leaveRepo.GetAllAsync(); - if (!string.IsNullOrWhiteSpace(status) && status != "全部") - { - leaves = leaves.Where(l => l.Status == status).ToList(); - } - var result = new List(); - foreach (var l in leaves.OrderByDescending(l => l.CreatedAt)) - { - result.Add(await ToLeaveDto(l)); - } - return result; - } - - public async Task ApproveLeaveAsync(long leaveId, long approverId, string approverName, ApproveLeaveDto dto) - { - var leave = await _leaveRepo.GetByIdAsync(leaveId); - if (leave == null) throw new ServiceException("请假申请不存在", 404); - if (leave.Status != "待审批") throw new ServiceException("该申请已处理"); - - var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == leaveId)) - .OrderBy(a => a.OrderNo).ToList(); - - // 校验当前审批节点 - var current = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo); - if (current == null) throw new ServiceException("审批节点不存在", 404); - if (current.ApproverId != approverId) - throw new ServiceException($"当前应由「{current.ApproverName}」审批,您无权操作", 403); - if (current.Status != "待审批") throw new ServiceException("该节点已处理"); - - // 更新当前节点 - var handleResult = dto.Approved ? "已批准" : "已驳回"; - current.Status = handleResult; - current.ApproveRemark = dto.Remark ?? ""; - current.ApprovedAt = DateTime.Now; - await _approverRepo.UpdateAsync(current); - - if (!dto.Approved) - { - // 驳回:整体驳回,结束流程 - leave.Status = "已驳回"; - leave.ApproverId = approverId; - leave.ApproverName = approverName; - leave.ApproveRemark = dto.Remark ?? ""; - leave.ApprovedAt = DateTime.Now; - await _leaveRepo.UpdateAsync(leave); - await _leaveRepo.SaveChangesAsync(); - await _approverRepo.SaveChangesAsync(); - - // 流程终结:完成该单所有待办 + 标记消息已读,避免残留未读红点 - await FinalizeLeaveApprovalAsync(leaveId, handleResult); - await NotifyLeaveResultAsync(leave, false, dto.Remark); - await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); - return await ToLeaveDto(leave); - } - - // 批准:判断是否还有下一级审批人 - var next = approvers.FirstOrDefault(a => a.OrderNo == leave.CurrentOrderNo + 1); - if (next != null) - { - // 流转到下一级审批人 - leave.CurrentOrderNo = next.OrderNo; - leave.ApproverId = next.ApproverId; - leave.ApproverName = next.ApproverName; - await _leaveRepo.UpdateAsync(leave); - await _leaveRepo.SaveChangesAsync(); - await _approverRepo.SaveChangesAsync(); - - // 完成当前审批人待办并标记其消息已读,再把同一条消息流转给下一审批人 - await CompleteCurrentApproverAsync(leaveId, approverId, handleResult); - await SendLeaveApprovalAsync(leave, next.ApproverId, next.OrderNo, approvers.Count); - } - else - { - // 最后一级审批人批准:整体批准 - leave.Status = "已批准"; - leave.ApproverId = approverId; - leave.ApproverName = approverName; - leave.ApproveRemark = dto.Remark ?? ""; - leave.ApprovedAt = DateTime.Now; - await _leaveRepo.UpdateAsync(leave); - await _leaveRepo.SaveChangesAsync(); - await _approverRepo.SaveChangesAsync(); - - // 流程终结 - await FinalizeLeaveApprovalAsync(leaveId, handleResult); - await NotifyLeaveResultAsync(leave, true, dto.Remark); - } - - // 业务状态已变更,推送事件让各端刷新(多端同步) - await _mediator.Publish(new BusinessChangedEvent("LeaveApproval", leaveId.ToString(), "StatusChanged")); - return await ToLeaveDto(leave); - } - - /// 查询审批人(admin 角色用户)ID 列表(未指定审批人时的兜底) - private async Task> GetApproverIdsAsync() - { - var adminRole = await _roleRepo.FirstOrDefaultAsync(r => r.Code == "admin"); - if (adminRole == null) return new List(); - var relations = await _empRoleRepo.FindAsync(ur => ur.RoleId == adminRole.Id); - return relations.Select(ur => ur.UserId).Distinct().ToList(); - } - - /// - /// 请假审批待办消息:一张请假单仅保留一条,随审批流转复用。 - /// 首次创建,流转时更新内容与当前审批人并重置为待投递(投递服务幂等,只推新审批人)。 - /// - private async Task SendLeaveApprovalAsync(LeaveRequest leave, long targetApproverId, int currentLevel, int totalLevel) - { - var businessType = NotificationTypeEnum.LeaveApproval.ToString(); - var businessId = leave.Id.ToString(); - var content = $"类型:{leave.Type},时间:{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm},事由:{leave.Reason}(第 {currentLevel}/{totalLevel} 级审批)"; - - var msg = await _msgRepo.FirstOrDefaultAsync(m => - m.BusinessType == businessType && m.BusinessId == businessId); - - if (msg == null) - { - var type = await _typeRepo.FirstOrDefaultAsync(t => t.Code == businessType); - var applicant = await _empRepo.GetByIdAsync(leave.EmployeeId); - msg = new NotificationMessage - { - NotificationTypeId = type?.Id ?? 0, - MessageTypeCode = businessType, - BusinessType = businessType, - BusinessId = businessId, - Title = $"{applicant?.UserName ?? "员工"} 提交了请假申请", - Content = content, - Priority = NotificationPriority.Important, - Status = NotificationStatus.Ready, - ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }), - CreatorId = leave.EmployeeId, - CreatorName = applicant?.UserName ?? "", - Category = NotificationCategory.Todo, - RoutePath = !string.IsNullOrWhiteSpace(type?.RouteTemplate) - ? type.RouteTemplate.Replace("{businessId}", businessId) - : $"/attendance/approve?leaveId={businessId}" - }; - await _msgRepo.AddAsync(msg); - await _msgRepo.SaveChangesAsync(); - } - else - { - // 复用:更新内容与当前审批人,重置为待投递,投递服务幂等只会推送给新审批人 - msg.Content = content; - msg.ForceReceivers = JsonSerializer.Serialize(new List { targetApproverId }); - msg.Status = NotificationStatus.Ready; - await _msgRepo.UpdateAsync(msg); - await _msgRepo.SaveChangesAsync(); - } - - // 当前审批人的待办(幂等:已存在则跳过) - var existTodo = await _todoRepo.FirstOrDefaultAsync(t => t.MessageId == msg.Id && t.AssigneeId == targetApproverId); - if (existTodo == null) - { - await _todoRepo.AddAsync(new TodoItem - { - MessageId = msg.Id, - AssigneeId = targetApproverId, - TodoStatus = TodoItemStatus.Pending - }); - await _todoRepo.SaveChangesAsync(); - } - } - - /// 完成当前审批人的待办,并标记其消息已读(流转时调用,避免已处理人残留未读红点) - private async Task CompleteCurrentApproverAsync(long leaveId, long approverId, string handleResult) - { - var msgs = await _msgRepo.FindAsync(m => - m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); - var msgIds = msgs.Select(m => m.Id).ToList(); - if (msgIds.Count == 0) return; - - var todos = await _todoRepo.FindAsync(t => - msgIds.Contains(t.MessageId) && t.AssigneeId == approverId && t.TodoStatus == TodoItemStatus.Pending); - foreach (var t in todos) - { - t.TodoStatus = TodoItemStatus.Completed; - t.CompletedTime = DateTime.Now; - t.HandleResult = handleResult; - await _todoRepo.UpdateAsync(t); - } - - var receivers = await _receiverRepo.FindAsync(r => - msgIds.Contains(r.MessageId) && r.UserId == approverId && r.ReadStatus == NotificationReadStatus.Unread); - foreach (var r in receivers) - { - r.ReadStatus = NotificationReadStatus.Read; - r.ReadTime = DateTime.Now; - await _receiverRepo.UpdateAsync(r); - } - } - - /// 流程终结:完成该单所有待处理待办,并标记所有未读消息为已读 - private async Task FinalizeLeaveApprovalAsync(long leaveId, string handleResult) - { - var msgs = await _msgRepo.FindAsync(m => - m.BusinessType == NotificationTypeEnum.LeaveApproval.ToString() && m.BusinessId == leaveId.ToString()); - var msgIds = msgs.Select(m => m.Id).ToList(); - if (msgIds.Count == 0) return; - - var todos = await _todoRepo.FindAsync(t => msgIds.Contains(t.MessageId) && t.TodoStatus == TodoItemStatus.Pending); - foreach (var t in todos) - { - t.TodoStatus = TodoItemStatus.Completed; - t.CompletedTime = DateTime.Now; - t.HandleResult = handleResult; - await _todoRepo.UpdateAsync(t); - } - - var receivers = await _receiverRepo.FindAsync(r => msgIds.Contains(r.MessageId) && r.ReadStatus == NotificationReadStatus.Unread); - foreach (var r in receivers) - { - r.ReadStatus = NotificationReadStatus.Read; - r.ReadTime = DateTime.Now; - await _receiverRepo.UpdateAsync(r); - } - } - - /// 通知申请人审批结果 - private async Task NotifyLeaveResultAsync(LeaveRequest leave, bool approved, string remark) - { - await _mediator.Send(new SendNotificationCommand( - NotificationTypeEnum.LeaveResult, - BusinessId: leave.Id.ToString(), - Title: approved ? "请假申请已批准" : "请假申请已驳回", - Content: $"您的请假({leave.Type},{leave.StartTime:MM-dd HH:mm} ~ {leave.EndTime:MM-dd HH:mm})已{(approved ? "批准" : "驳回")}。审批意见:{remark}", - ForceReceivers: new List { leave.EmployeeId } - )); - } - - private async Task ToLeaveDto(LeaveRequest l) - { - var emp = await _empRepo.GetByIdAsync(l.EmployeeId); - var dept = emp != null ? await _deptRepo.GetByIdAsync(emp.DepartmentId) : null; - - var approvers = (await _approverRepo.FindAsync(a => a.LeaveId == l.Id)) - .OrderBy(a => a.OrderNo) - .Select(a => new LeaveApproverDto - { - Id = a.Id, - ApproverId = a.ApproverId, - ApproverName = a.ApproverName, - OrderNo = a.OrderNo, - Status = a.Status, - ApproveRemark = a.ApproveRemark, - ApprovedAt = a.ApprovedAt - }) - .ToList(); - - return new LeaveRequestDto - { - Id = l.Id, - EmployeeId = l.EmployeeId, - EmployeeName = emp?.UserName ?? "", - DepartmentName = dept?.Name ?? "", - Type = l.Type, - StartTime = l.StartTime, - EndTime = l.EndTime, - Reason = l.Reason, - Status = l.Status, - CurrentOrderNo = l.CurrentOrderNo, - ApproverName = l.ApproverName, - ApproveRemark = l.ApproveRemark, - ApprovedAt = l.ApprovedAt, - CreatedAt = l.CreatedAt, - Approvers = approvers - }; - } -} diff --git a/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs b/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs deleted file mode 100644 index 2814976..0000000 --- a/backend/F9WebAiTest.Application/Services/Attendance/IAttendanceService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using F9WebAiTest.Application.Common; -using F9WebAiTest.Application.Dtos; -using F9WebAiTest.Application.Dtos.Attendance; - -namespace F9WebAiTest.Application.Services.Attendance; - -public interface IAttendanceService -{ - // 打卡 - Task PunchAsync(long employeeId); - - // 我的考勤记录(分页) - Task> GetMyRecordsAsync(long employeeId, int pageIndex, int pageSize); - - // 我的考勤统计 - Task GetMySummaryAsync(long employeeId, string month); - - // 全部考勤记录(管理员) - Task> GetAllRecordsAsync(AttendanceQueryDto query); - - // 请假 - Task ApplyLeaveAsync(long employeeId, CreateLeaveDto dto); - Task> GetMyLeavesAsync(long employeeId); - Task> GetAllLeavesAsync(string status); - Task ApproveLeaveAsync(long leaveId, long approverId, string approverName, ApproveLeaveDto dto); -} diff --git a/backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs b/backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs new file mode 100644 index 0000000..59e7e6f --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/PayRoll/CalcLdcctService.cs @@ -0,0 +1,819 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.PayRoll; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using SysEmp = F9WebAiTest.Domain.Entities.SYS.Employees; +using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes; + +namespace F9WebAiTest.Application.Services.PayRoll; + +/// +/// 薪资计算-立缫挡车车头(挡车工 / 车头工 产量计算) +/// 对应 F9 WinClient\F9.Business.PayRoll\PAYROLL\CALCLDCCT(Page_CalcLdcct + Page_CalcLdcct_Edit) +/// 数据源:看台记录(挡车/车头)、煮茧日报(今缫桶数)、落丝记录(茧量)、秤大丝(丝量/缫折)、 +/// 纤度检验(纤度明细/均方差/总差)、黑板检验(二度/三度/清洁/洁净)、生丝规格、生丝定级指标。 +/// 配置:工艺庄口薪资配置、工种薪资配置、挡车岗位薪资配置、挡车纤度奖扣、薪资价格配置。 +/// +public interface ICalcLdcctService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime start, DateTime end, + long? depId, long? worktypeId, string like); + Task CalcDcgAsync(CalcLdcctCalcDto dto); + Task CalcCtgAsync(CalcLdcctCalcDto dto); + Task SaveAsync(CalcLdcctSaveDto dto); + Task DeleteAsync(DateTime start, DateTime end, long depId, long worktypeId); +} + +public class CalcLdcctService : ICalcLdcctService +{ + private readonly IRepository _repo; + private readonly IRepository _dcgRepo; + private readonly IRepository _ctgRepo; + private readonly IRepository _zjrbRepo; + private readonly IRepository _zjrbJizuRepo; + private readonly IRepository _lsjlRepo; + private readonly IRepository _cdsLsjlRepo; + private readonly IRepository _cdsChetaiRepo; + private readonly IRepository _xdjyLsjlRepo; + private readonly IRepository _xdjyRecordRepo; + private readonly IRepository _hbjyRepo; + private readonly IRepository _confGyzkRepo; + private readonly IRepository _confGzRepo; + private readonly IRepository _confDcgRepo; + private readonly IRepository _xdjkRepo; + private readonly IRepository _xzjgRepo; + private readonly IRepository _guigeRepo; + private readonly IRepository _ssdjzbRepo; + private readonly IRepository _zkRepo; + private readonly IRepository _depRepo; + private readonly IRepository _empRepo; + private readonly IRepository _wtRepo; + + public CalcLdcctService( + IRepository repo, + IRepository dcgRepo, + IRepository ctgRepo, + IRepository zjrbRepo, + IRepository zjrbJizuRepo, + IRepository lsjlRepo, + IRepository cdsLsjlRepo, + IRepository cdsChetaiRepo, + IRepository xdjyLsjlRepo, + IRepository xdjyRecordRepo, + IRepository hbjyRepo, + IRepository confGyzkRepo, + IRepository confGzRepo, + IRepository confDcgRepo, + IRepository xdjkRepo, + IRepository xzjgRepo, + IRepository guigeRepo, + IRepository ssdjzbRepo, + IRepository zkRepo, + IRepository depRepo, + IRepository empRepo, + IRepository wtRepo) + { + _repo = repo; _dcgRepo = dcgRepo; _ctgRepo = ctgRepo; + _zjrbRepo = zjrbRepo; _zjrbJizuRepo = zjrbJizuRepo; _lsjlRepo = lsjlRepo; + _cdsLsjlRepo = cdsLsjlRepo; _cdsChetaiRepo = cdsChetaiRepo; + _xdjyLsjlRepo = xdjyLsjlRepo; _xdjyRecordRepo = xdjyRecordRepo; _hbjyRepo = hbjyRepo; + _confGyzkRepo = confGyzkRepo; _confGzRepo = confGzRepo; _confDcgRepo = confDcgRepo; + _xdjkRepo = xdjkRepo; _xzjgRepo = xzjgRepo; _guigeRepo = guigeRepo; _ssdjzbRepo = ssdjzbRepo; + _zkRepo = zkRepo; _depRepo = depRepo; _empRepo = empRepo; _wtRepo = wtRepo; + } + + // ==================== 常量(对齐 F9 Page_CalcLdcct) ==================== + private const decimal XushuPerTai = 20m; // 每台绪数 + private const decimal CtgTaiShu = 20m; // 车头工台时按 20 台/组 + private const decimal CtgHoursDiv = 9m; // 车头基础单价 /9(标准上班时长) + private const decimal CtgRenShu = 2m; // 车头标准人数 2 + private const decimal DengjiStep = 0.1m; // 等级分步长 + private const int SaozheDeadZone = 2; // 缫折死区 ±2kg + private const int DcgSaozheCap = 10; // 挡车缫折封顶 ±10 + private const int CtgSaozheCap = 3; // 车头缫折封顶 ±3 + private const string PriceGroup = "薪资.前缫"; + private const string DcgDengjiPriceKey = "挡车工计件等级奖扣单价"; + private const string CtgDengjiPriceKey = "车头工计件等级奖扣单价"; + + // ==================== 查询 ==================== + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime start, + DateTime end, long? depId, long? worktypeId, string like) + { + var all = await _repo.GetAllAsync(); + var from = start.Date; + var to = end.Date; + var q = all.AsQueryable().Where(x => x.Date.Date >= from && x.Date.Date <= to); + if (depId.HasValue) q = q.Where(x => x.DepId == depId.Value); + if (worktypeId.HasValue) q = q.Where(x => x.WorktypeId == worktypeId.Value); + + var deps = await _depRepo.GetAllAsync(); + var emps = await _empRepo.GetAllAsync(); + var wts = await _wtRepo.GetAllAsync(); + var zks = await _zkRepo.GetAllAsync(); + var confGyzks = await _confGyzkRepo.GetAllAsync(); + + var depDict = deps.ToDictionary(d => d.Id, d => d.Name); + var empDict = emps.ToDictionary(e => e.Id, e => e.Name); + var wtDict = wts.ToDictionary(w => w.Id, w => w.WorkName); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + var confDict = confGyzks.GroupBy(c => $"{c.Year}-{c.Month}-{c.GyzkId}") + .ToDictionary(g => g.Key, g => g.OrderByDescending(c => c.Id).First()); + + if (!string.IsNullOrWhiteSpace(like)) + { + var empIds = emps.Where(e => e.Name.Contains(like)).Select(e => e.Id).ToHashSet(); + var zkIds = zks.Where(z => z.ZhuangKouName.Contains(like)).Select(z => z.Id).ToHashSet(); + q = q.Where(x => empIds.Contains(x.EmployeeId) || zkIds.Contains(x.GyzkId)); + } + + var total = q.LongCount(); + var items = q.OrderBy(x => x.Date).ThenBy(x => x.Jizu).ThenBy(x => x.Banci).ThenBy(x => x.EmployeeId) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + confDict.TryGetValue($"{x.Date.Year}-{x.Date.Month}-{x.GyzkId}", out var cfg); + return new CalcLdcctListItemDto + { + Id = x.Id, Date = x.Date, EmployeeId = x.EmployeeId, + EmployeeName = empDict.GetValueOrDefault(x.EmployeeId, ""), + DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + WorktypeId = x.WorktypeId, WorktypeName = wtDict.GetValueOrDefault(x.WorktypeId, ""), + GyzkId = x.GyzkId, GyzkName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Guige = x.Guige, Chetai = x.Chetai, Jizu = x.Jizu, Banci = x.Banci, + BanciName = BanciName(x.Banci), + HoursKantai = x.HoursKantai, Dangcheshu = x.Dangcheshu, + Dengji = x.Dengji, Dengjifen = x.Dengjifen, + GyzkTaichan = cfg?.QsTaichan ?? 0, GyzkSaozhe = cfg?.SaoZhe ?? 0, + ClDanjiaBase = x.ClDanjiaBase, ClDanjiaDengji = x.ClDanjiaDengji, + ClDanjiaActual = x.ClDanjiaActual, ClSiliang = x.ClSiliang, + ZlSaozhe = x.ZlSaozhe, ZlCenterxd = x.ZlCenterxd, ZlYexiannum = x.ZlYexiannum, + ZlQingjie = x.ZlQingjie, ZlJiejing = x.ZlJiejing, ZlErdu = x.ZlErdu, ZlSandu = x.ZlSandu, + JineCl_Original = x.JineCl_Original, Xishu_JineCl = x.Xishu_JineCl, JineCl = x.JineCl, + JineZlSaozhe = x.JineZlSaozhe, JineZlCenterxd = x.JineZlCenterxd, JineZlYexian = x.JineZlYexian, + JineZlQingjie = x.JineZlQingjie, JineZlJiejing = x.JineZlJiejing, + JineZlErdu = x.JineZlErdu, JineZlSandu = x.JineZlSandu, + JineZlTotal = x.JineZlSaozhe + x.JineZlCenterxd + x.JineZlYexian + x.JineZlQingjie + + x.JineZlJiejing + x.JineZlErdu + x.JineZlSandu + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + // ==================== 一键计算:挡车工 ==================== + + public async Task CalcDcgAsync(CalcLdcctCalcDto dto) + { + var ctx = await BuildContextAsync(dto); + var result = new CalcLdcctResultDto(); + if (ctx.DcgKtjl.Count == 0) + { + result.Warnings.Add("该账期该部门没有挡车工看台记录"); + return result; + } + + var rows = new List(); + + // 按「日 + 机组」组织 + var jizuDays = ctx.DcgKtjl.GroupBy(k => (k.Date.Date, k.Jizu)); + foreach (var jd in jizuDays) + { + var (date, jizu) = jd.Key; + var ktjls = jd.Where(k => !k.NoChanliang).ToList(); + if (ktjls.Count == 0) continue; + + // 该机组当日各庄口的今缫桶数(煮茧日报不分班) + var gyzkTongshu = ctx.GyzkTongshuByJizuDay.TryGetValue((date, jizu), out var gt) + ? gt : new List<(long GyzkId, string Guige, int Tongshu, int StdLuosi)>(); + + var tongshuSum = gyzkTongshu.Sum(g => g.Tongshu); + var banciNum = ktjls.Select(k => k.Banci).Distinct().Count(); + var half = banciNum >= 2 ? 0.5m : 1m; + + foreach (var (gyzkId, guige, tongshu, stdLuosi) in gyzkTongshu) + { + var cfg = ctx.ConfGyzk(gyzkId, date); + if (cfg == null) + { + result.Warnings.Add($"{date:yyyy-MM-dd} 机组 {jizu} 庄口缺少薪资配置,已跳过"); + continue; + } + var guigeCfg = ctx.Guige(guige); + + // 每桶丝量 = 每车丝量(kg→g) / 标准落丝桶数 + if (stdLuosi <= 0 || cfg.MeiCheSiLiang <= 0) + { + result.Warnings.Add($"{date:yyyy-MM-dd} 庄口 {cfg.Guige} 缺少每车丝量/标准落丝桶数,已跳过"); + continue; + } + var meitongSiliang = cfg.MeiCheSiLiang * 1000m / stdLuosi; + var jiayiSumSiliang = Math.Round(tongshu * meitongSiliang, 0, MidpointRounding.AwayFromZero); + + var rate = tongshuSum > 0 ? (decimal)tongshu / tongshuSum : 0m; + + // 按班次拆分丝量:按 台时 = 挡车数 × 时长 加权 + var jizuDayTaishi = ktjls.Sum(k => k.DangcheNum * k.Hours); + foreach (var banciGroup in ktjls.GroupBy(k => k.Banci)) + { + var banciTaishi = banciGroup.Sum(k => k.DangcheNum * k.Hours); + var banciSiliang = jizuDayTaishi > 0 + ? Math.Round(jiayiSumSiliang * (banciTaishi / jizuDayTaishi), 0, MidpointRounding.AwayFromZero) + : 0m; + var groupTaishi = banciTaishi; + + foreach (var k in banciGroup) + { + // 看台时长:按庄口今缫桶数占比分摊个人工时 + var hoursKantai = Math.Round(k.Hours * rate, 1, MidpointRounding.AwayFromZero); + + var siliang = groupTaishi > 0 + ? Math.Round(banciSiliang * (k.DangcheNum * k.Hours / groupTaishi), 0, MidpointRounding.AwayFromZero) + : 0m; + + // ---- 质量(按车台看台比例分摊) ---- + var quality = CalcDcgQuality(ctx, date, jizu, gyzkId, k, half, cfg, guigeCfg); + + // ---- 等级(木桶短板) ---- + var (dengji, dengjifen) = CalDengJi(ctx, guige, quality.Junfangcha, quality.Zongcha, + quality.QingjieFen, quality.JiejingFen, quality.Erdu, quality.Sandu, cfg); + + // ---- 单价 ---- + var xushu = k.DangcheNum * XushuPerTai; + var gangweiBase = GetDcgDanJiaBase(xushu, ctx.ConfDcg); + if (ctx.ConfGz == null || ctx.ConfGz.QqDaysStandard <= 0 || ctx.ConfGz.HoursPerday <= 0 + || cfg.QsTaichan <= 0 || k.DangcheNum <= 0) + { + result.Warnings.Add($"工种薪资配置或庄口台产缺失,{date:yyyy-MM-dd} {k.EmployeeId} 已跳过"); + continue; + } + var danjiaBase = Math.Round(gangweiBase / ctx.ConfGz.QqDaysStandard / k.DangcheNum + / ctx.ConfGz.HoursPerday / cfg.QsTaichan, 7, MidpointRounding.AwayFromZero); + if (danjiaBase == 0) continue; + + var danjiaDengji = Math.Round( + (dengjifen - cfg.QsDengjifen) / DengjiStep * ctx.DcgJjDengJiJkPrice / 10m / 1000m, + 7, MidpointRounding.AwayFromZero); + var danjiaActual = Math.Round(danjiaBase + danjiaDengji, 7, MidpointRounding.AwayFromZero); + + // ---- 金额 ---- + var e = new CalcLdcct + { + Date = date, EmployeeId = k.EmployeeId, DepId = dto.DepId, WorktypeId = dto.WorktypeId, + Jizu = jizu, Banci = (int)k.Banci, GyzkId = gyzkId, Guige = guige, + Chetai = k.ChetaiStart == k.ChetaiEnd ? k.ChetaiStart.ToString() : $"{k.ChetaiStart}-{k.ChetaiEnd}", + HoursKantai = hoursKantai, Dangcheshu = k.DangcheNum, Dengji = dengji, Dengjifen = dengjifen, + ClDanjiaBase = danjiaBase, ClDanjiaDengji = danjiaDengji, ClDanjiaActual = danjiaActual, + ClSiliang = siliang, + ZlSaozhe = quality.Saozhe, ZlCenterxd = quality.CenterNum, ZlYexiannum = quality.YexianNum, + ZlQingjie = quality.QingjiePianshu, ZlJiejing = quality.JiejingPianshu, + ZlErdu = quality.Erdu, ZlSandu = quality.Sandu, + Xishu_JineCl = cfg.DcgJijianXishu + }; + e.JineCl_Original = Math.Round(danjiaActual * siliang, 2, MidpointRounding.AwayFromZero); + e.JineCl = Math.Round(danjiaActual * siliang * cfg.DcgJijianXishu, 2, MidpointRounding.AwayFromZero); + e.JineZlSaozhe = CalJineSaozhe(cfg.SaoZhe, quality.Saozhe, siliang, DcgSaozheCap, 5m); + e.JineZlCenterxd = Math.Round(quality.CenterNum * 1m, 2, MidpointRounding.AwayFromZero); + e.JineZlYexian = -Math.Round(quality.YexianKouKuan, 2, MidpointRounding.AwayFromZero); + e.JineZlQingjie = -Math.Round(quality.QingjieKouKuan, 2, MidpointRounding.AwayFromZero); + e.JineZlJiejing = -Math.Round(quality.JiejingKouKuan, 2, MidpointRounding.AwayFromZero); + e.JineZlErdu = -Math.Round(quality.Erdu * 2m, 2, MidpointRounding.AwayFromZero); + e.JineZlSandu = -Math.Round(quality.Sandu * 20m, 2, MidpointRounding.AwayFromZero); + + rows.Add(e); + } + } + } + } + + return await PersistAsync(rows, dto, result); + } + + // ==================== 一键计算:车头工 ==================== + + public async Task CalcCtgAsync(CalcLdcctCalcDto dto) + { + var ctx = await BuildContextAsync(dto); + var result = new CalcLdcctResultDto(); + if (ctx.CtgKtjl.Count == 0) + { + result.Warnings.Add("该账期该部门没有车头工看台记录"); + return result; + } + + // 车头工强依赖已存的挡车工产量 + var dcgRows = (await _repo.GetAllAsync()) + .Where(x => x.Date.Date >= dto.StartDate.Date && x.Date.Date <= dto.EndDate.Date + && x.DepId == dto.DepId && x.WorktypeId == ctx.DcgWorktypeId).ToList(); + + var rows = new List(); + + var jizuDays = ctx.CtgKtjl.GroupBy(k => (k.Date.Date, k.Jizu)); + foreach (var jd in jizuDays) + { + var (date, jizu) = jd.Key; + var ktjls = jd.Where(k => !k.NoChanliang).ToList(); + if (ktjls.Count == 0) continue; + + var gyzkTongshu = ctx.GyzkTongshuByJizuDay.TryGetValue((date, jizu), out var gt) + ? gt : new List<(long GyzkId, string Guige, int Tongshu, int StdLuosi)>(); + var tongshuSum = gyzkTongshu.Sum(g => g.Tongshu); + var banciNum = ktjls.Select(k => k.Banci).Distinct().Count(); + var half = banciNum >= 2 ? 0.5m : 1m; + + foreach (var (gyzkId, guige, tongshu, stdLuosi) in gyzkTongshu) + { + var cfg = ctx.ConfGyzk(gyzkId, date); + if (cfg == null) + { + result.Warnings.Add($"{date:yyyy-MM-dd} 机组 {jizu} 庄口缺少薪资配置,已跳过"); + continue; + } + if (stdLuosi <= 0 || cfg.MeiCheSiLiang <= 0) + { + result.Warnings.Add($"{date:yyyy-MM-dd} 庄口 {cfg.Guige} 缺少每车丝量/标准落丝桶数,已跳过"); + continue; + } + var meitongSiliang = cfg.MeiCheSiLiang * 1000m / stdLuosi; + var jiayiSumSiliang = Math.Round(tongshu * meitongSiliang, 0, MidpointRounding.AwayFromZero); + var rate = tongshuSum > 0 ? (decimal)tongshu / tongshuSum : 0m; + + // 全组汇总质量(落丝主表,不分摊) + var lsjlIds = ctx.LsjlIdsByJizuDayGyzk.TryGetValue((date, jizu, gyzkId), out var ids) + ? ids : new List(); + var saozheList = new List(); + var erduSum = 0m; var sanduSum = 0m; + foreach (var lid in lsjlIds) + { + if (ctx.CdsLsjl.TryGetValue(lid, out var cds) && cds.Siliang > 0 && cds.Jianliang > 0) + saozheList.Add((decimal)cds.Jianliang * 100m / cds.Siliang); + if (ctx.HbjyByLsjl.TryGetValue(lid, out var hb)) + { + erduSum += hb.Sum(h => h.YdErdu); + sanduSum += hb.Sum(h => h.YdSandu); + } + } + var zlSaozhe = saozheList.Count > 0 + ? Math.Round(saozheList.Average(), 0, MidpointRounding.AwayFromZero) : cfg.SaoZhe; + + var jizuDayTaishi = ktjls.Sum(k => CtgTaiShu * k.Hours); + + foreach (var banciGroup in ktjls.GroupBy(k => k.Banci)) + { + var banciTaishi = banciGroup.Sum(k => CtgTaiShu * k.Hours); + var banciSiliang = jizuDayTaishi > 0 + ? Math.Round(jiayiSumSiliang * (banciTaishi / jizuDayTaishi), 0, MidpointRounding.AwayFromZero) + : 0m; + var maxHours = banciGroup.Max(k => k.Hours); + + foreach (var k in banciGroup) + { + var hoursKantai = Math.Round(k.Hours * rate, 1, MidpointRounding.AwayFromZero); + var siliang = maxHours > 0 + ? Math.Round(banciSiliang * (k.Hours / (maxHours * CtgRenShu)), 0, MidpointRounding.AwayFromZero) + : 0m; + + // 等级:按挡车工丝量加权平均等级分再回落取整档 + var (dengji, dengjifen) = CalCtgDengji(ctx, dcgRows, date, jizu, gyzkId, guige, cfg); + + if (ctx.ConfGz == null || ctx.ConfGz.QqDaysStandard <= 0 || cfg.QsTaichan <= 0) + { + result.Warnings.Add($"工种薪资配置缺失,{date:yyyy-MM-dd} {k.EmployeeId} 已跳过"); + continue; + } + var danjiaBase = Math.Round(ctx.ConfDcg.JineCtg / ctx.ConfGz.QqDaysStandard / CtgHoursDiv + / CtgTaiShu / cfg.QsTaichan * CtgRenShu, 7, MidpointRounding.AwayFromZero); + if (danjiaBase == 0) continue; + + var danjiaDengji = Math.Round( + (dengjifen - cfg.QsDengjifen) / DengjiStep * ctx.CtgJjDengJiJkPrice / 10m / 1000m, + 7, MidpointRounding.AwayFromZero); + var danjiaActual = Math.Round(danjiaBase + danjiaDengji, 7, MidpointRounding.AwayFromZero); + + var e = new CalcLdcct + { + Date = date, EmployeeId = k.EmployeeId, DepId = dto.DepId, WorktypeId = dto.WorktypeId, + Jizu = jizu, Banci = (int)k.Banci, GyzkId = gyzkId, Guige = guige, Chetai = "", + HoursKantai = hoursKantai, Dangcheshu = 0, Dengji = dengji, Dengjifen = dengjifen, + ClDanjiaBase = danjiaBase, ClDanjiaDengji = danjiaDengji, ClDanjiaActual = danjiaActual, + ClSiliang = siliang, + ZlSaozhe = zlSaozhe, ZlCenterxd = 0, ZlYexiannum = 0, ZlQingjie = 0, ZlJiejing = 0, + ZlErdu = Math.Round(erduSum * half, 2, MidpointRounding.AwayFromZero), + ZlSandu = Math.Round(sanduSum * half, 2, MidpointRounding.AwayFromZero), + Xishu_JineCl = cfg.CtgJijianXishu + }; + e.JineCl_Original = Math.Round(danjiaActual * siliang, 2, MidpointRounding.AwayFromZero); + e.JineCl = Math.Round(danjiaActual * siliang * cfg.CtgJijianXishu, 2, MidpointRounding.AwayFromZero); + e.JineZlSaozhe = CalJineSaozhe(cfg.SaoZhe, zlSaozhe, siliang, CtgSaozheCap, 1m); + e.JineZlCenterxd = 0; e.JineZlYexian = 0; e.JineZlQingjie = 0; e.JineZlJiejing = 0; + e.JineZlErdu = -Math.Round(e.ZlErdu * 2m, 2, MidpointRounding.AwayFromZero); + e.JineZlSandu = -Math.Round(e.ZlSandu * 20m, 2, MidpointRounding.AwayFromZero); + + rows.Add(e); + } + } + } + } + + return await PersistAsync(rows, dto, result); + } + + // ==================== 保存 / 删除 ==================== + + public async Task SaveAsync(CalcLdcctSaveDto dto) + { + var olds = (await _repo.GetAllAsync()) + .Where(x => x.Date.Date >= dto.StartDate.Date && x.Date.Date <= dto.EndDate.Date + && x.DepId == dto.DepId && x.WorktypeId == dto.WorktypeId).ToList(); + if (olds.Count > 0) await _repo.HardDeleteRangeAsync(olds); + + foreach (var it in dto.Items) + { + var e = new CalcLdcct + { + Date = it.Date.Date, EmployeeId = it.EmployeeId, DepId = it.DepId, WorktypeId = it.WorktypeId, + Jizu = it.Jizu, Banci = it.Banci, GyzkId = it.GyzkId, Guige = it.Guige ?? "", Chetai = it.Chetai ?? "", + HoursKantai = it.HoursKantai, Dangcheshu = it.Dangcheshu, Dengji = it.Dengji ?? "", Dengjifen = it.Dengjifen, + ClDanjiaBase = it.ClDanjiaBase, ClDanjiaDengji = it.ClDanjiaDengji, ClDanjiaActual = it.ClDanjiaActual, + ClSiliang = it.ClSiliang, + ZlSaozhe = it.ZlSaozhe, ZlCenterxd = it.ZlCenterxd, ZlYexiannum = it.ZlYexiannum, + ZlQingjie = it.ZlQingjie, ZlJiejing = it.ZlJiejing, ZlErdu = it.ZlErdu, ZlSandu = it.ZlSandu, + JineCl_Original = it.JineCl_Original, Xishu_JineCl = it.Xishu_JineCl, JineCl = it.JineCl, + JineZlSaozhe = it.JineZlSaozhe, JineZlCenterxd = it.JineZlCenterxd, JineZlYexian = it.JineZlYexian, + JineZlQingjie = it.JineZlQingjie, JineZlJiejing = it.JineZlJiejing, + JineZlErdu = it.JineZlErdu, JineZlSandu = it.JineZlSandu + }; + await _repo.AddAsync(e); + } + return dto.Items.Count; + } + + public async Task DeleteAsync(DateTime start, DateTime end, long depId, long worktypeId) + { + var olds = (await _repo.GetAllAsync()) + .Where(x => x.Date.Date >= start.Date && x.Date.Date <= end.Date + && x.DepId == depId && x.WorktypeId == worktypeId).ToList(); + if (olds.Count > 0) await _repo.HardDeleteRangeAsync(olds); + } + + // ==================== 内部:上下文 ==================== + + private class CalcContext + { + public List DcgKtjl = new(); + public List CtgKtjl = new(); + public long DcgWorktypeId; + public Dictionary<(DateTime, int), List<(long GyzkId, string Guige, int Tongshu, int StdLuosi)>> GyzkTongshuByJizuDay = new(); + public Dictionary<(DateTime, int, long), List> LsjlIdsByJizuDayGyzk = new(); + public Dictionary CdsLsjl = new(); + public Dictionary> CdsChetai = new(); + public Dictionary> XdjyByLsjl = new(); + public Dictionary> HbjyByLsjl = new(); + public Dictionary> ConfByGyzk = new(); + public Dictionary GuigeDict = new(); + public Dictionary> YeXianJk = new(); + public List Djzb = new(); + public ConfGz ConfGz = null!; + public ConfDcg ConfDcg = null!; + public decimal DcgJjDengJiJkPrice; + public decimal CtgJjDengJiJkPrice; + public List Warnings = new(); + + public ConfGyzk ConfGyzk(long gyzkId, DateTime date) => + ConfByGyzk.TryGetValue(gyzkId, out var list) + ? list.FirstOrDefault(c => c.Year == date.Year && c.Month == date.Month) + ?? list.OrderByDescending(c => c.Year).ThenByDescending(c => c.Month).FirstOrDefault() + : null; + + public ShengsiGuige Guige(string guige) => + GuigeDict.TryGetValue(guige ?? "", out var g) ? g : null; + + public List YeXianJiangKou(string guige) => + YeXianJk.TryGetValue(guige ?? "", out var l) ? l : new List(); + } + + private async Task BuildContextAsync(CalcLdcctCalcDto dto) + { + var ctx = new CalcContext(); + var from = dto.StartDate.Date; + var to = dto.EndDate.Date; + + ctx.DcgKtjl = (await _dcgRepo.GetAllAsync()) + .Where(x => x.Date.Date >= from && x.Date.Date <= to && x.DeptId == dto.DepId).ToList(); + ctx.CtgKtjl = (await _ctgRepo.GetAllAsync()) + .Where(x => x.Date.Date >= from && x.Date.Date <= to && x.DeptId == dto.DepId).ToList(); + + // 挡车工工种:取看台记录中出现最多的工种(车头计算需按挡车工工种取已存产量) + ctx.DcgWorktypeId = ctx.DcgKtjl.GroupBy(k => k.WorktypeId).OrderByDescending(g => g.Count()) + .Select(g => g.Key).FirstOrDefault(); + + // 煮茧日报:日 + 机组 → 各庄口今缫桶数(不分班) + var zjrbs = (await _zjrbRepo.GetAllAsync()) + .Where(x => x.Date.Date >= from && x.Date.Date <= to && x.DeptId == dto.DepId).ToList(); + var zjrbJizus = await _zjrbJizuRepo.GetAllAsync(); + foreach (var z in zjrbs) + { + foreach (var jz in zjrbJizus.Where(j => j.ZjrbId == z.Id)) + { + var key = (z.Date.Date, jz.Jizu); + if (!ctx.GyzkTongshuByJizuDay.TryGetValue(key, out var list)) + ctx.GyzkTongshuByJizuDay[key] = list = new List<(long, string, int, int)>(); + list.Add((z.GyzkId, z.Guige, jz.TongshuJinsao, z.StandardLuositongshu)); + } + } + + // 落丝记录:日 + 机组 + 庄口 → lsjlId 列表 + var lsjls = (await _lsjlRepo.GetAllAsync()) + .Where(x => x.WorkDate.Date >= from && x.WorkDate.Date <= to && x.DeptId == dto.DepId).ToList(); + foreach (var l in lsjls) + { + var key = (l.WorkDate.Date, l.Jizu, l.GyzkId); + if (!ctx.LsjlIdsByJizuDayGyzk.TryGetValue(key, out var list)) + ctx.LsjlIdsByJizuDayGyzk[key] = list = new List(); + list.Add(l.Id); + } + var allLsjlIds = lsjls.Select(l => l.Id).ToHashSet(); + + ctx.CdsLsjl = (await _cdsLsjlRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId)) + .GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.First()); + ctx.CdsChetai = (await _cdsChetaiRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId)) + .GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.ToList()); + ctx.XdjyByLsjl = (await _xdjyRecordRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId)) + .GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.ToList()); + ctx.HbjyByLsjl = (await _hbjyRepo.GetAllAsync()).Where(x => allLsjlIds.Contains(x.LsjlId)) + .GroupBy(x => x.LsjlId).ToDictionary(g => g.Key, g => g.ToList()); + _ = await _xdjyLsjlRepo.GetAllAsync(); // 纤度汇总(等级参考,保留实体引用) + + ctx.ConfByGyzk = (await _confGyzkRepo.GetAllAsync()) + .GroupBy(c => c.GyzkId).ToDictionary(g => g.Key, g => g.ToList()); + ctx.GuigeDict = (await _guigeRepo.GetAllAsync()) + .GroupBy(g => g.Guige).ToDictionary(g => g.Key, g => g.First()); + ctx.Djzb = (await _ssdjzbRepo.GetAllAsync()).Where(x => x.Standard == 2).ToList(); // 2=个人 + ctx.YeXianJk = (await _xdjkRepo.GetAllAsync()) + .GroupBy(x => x.GuiGe).ToDictionary(g => g.Key, g => g.OrderBy(x => x.XianDu).ToList()); + ctx.ConfGz = await _confGzRepo.FirstOrDefaultAsync(x => x.WorkTypeId == dto.WorktypeId); + ctx.ConfDcg = await _confDcgRepo.FirstOrDefaultAsync(x => x.DepId == dto.DepId) + ?? new ConfDcg { DepId = dto.DepId }; + ctx.DcgJjDengJiJkPrice = await PriceAsync(DcgDengjiPriceKey); + ctx.CtgJjDengJiJkPrice = await PriceAsync(CtgDengjiPriceKey); + + return ctx; + } + + private async Task PriceAsync(string key) + { + var e = await _xzjgRepo.FirstOrDefaultAsync(x => x.Group == PriceGroup && x.Key == key); + return e?.Price ?? 0m; + } + + private async Task PersistAsync(List rows, CalcLdcctCalcDto dto, CalcLdcctResultDto result) + { + await SaveAsync(new CalcLdcctSaveDto + { + StartDate = dto.StartDate, EndDate = dto.EndDate, DepId = dto.DepId, WorktypeId = dto.WorktypeId, + Items = rows.Select(r => new CalcLdcctSaveItemDto + { + Date = r.Date, EmployeeId = r.EmployeeId, DepId = r.DepId, WorktypeId = r.WorktypeId, + Jizu = r.Jizu, Banci = r.Banci, GyzkId = r.GyzkId, Guige = r.Guige, Chetai = r.Chetai, + HoursKantai = r.HoursKantai, Dangcheshu = r.Dangcheshu, Dengji = r.Dengji, Dengjifen = r.Dengjifen, + ClDanjiaBase = r.ClDanjiaBase, ClDanjiaDengji = r.ClDanjiaDengji, ClDanjiaActual = r.ClDanjiaActual, + ClSiliang = r.ClSiliang, + ZlSaozhe = r.ZlSaozhe, ZlCenterxd = r.ZlCenterxd, ZlYexiannum = r.ZlYexiannum, + ZlQingjie = r.ZlQingjie, ZlJiejing = r.ZlJiejing, ZlErdu = r.ZlErdu, ZlSandu = r.ZlSandu, + JineCl_Original = r.JineCl_Original, Xishu_JineCl = r.Xishu_JineCl, JineCl = r.JineCl, + JineZlSaozhe = r.JineZlSaozhe, JineZlCenterxd = r.JineZlCenterxd, JineZlYexian = r.JineZlYexian, + JineZlQingjie = r.JineZlQingjie, JineZlJiejing = r.JineZlJiejing, + JineZlErdu = r.JineZlErdu, JineZlSandu = r.JineZlSandu + }).ToList() + }); + + result.Count = rows.Count; + result.SumSiliang = rows.Sum(r => r.ClSiliang); + result.SumJineCl = Math.Round(rows.Sum(r => r.JineCl), 2, MidpointRounding.AwayFromZero); + result.SumJineZl = Math.Round(rows.Sum(r => r.JineZlSaozhe + r.JineZlCenterxd + r.JineZlYexian + + r.JineZlQingjie + r.JineZlJiejing + r.JineZlErdu + r.JineZlSandu), 2, MidpointRounding.AwayFromZero); + return result; + } + + // ==================== 内部:挡车工质量 ==================== + + private class DcgQuality + { + public decimal Saozhe; + public decimal CenterNum; + public decimal YexianNum; + public decimal YexianKouKuan; + public decimal QingjiePianshu; + public decimal QingjieKouKuan; + public decimal JiejingPianshu; + public decimal JiejingKouKuan; + public decimal Erdu; + public decimal Sandu; + public double Junfangcha; + public double Zongcha; + public decimal QingjieFen; + public decimal JiejingFen; + } + + private DcgQuality CalcDcgQuality(CalcContext ctx, DateTime date, int jizu, long gyzkId, KtjlDcg k, + decimal half, ConfGyzk cfg, ShengsiGuige guigeCfg) + { + var q = new DcgQuality(); + var lsjlIds = ctx.LsjlIdsByJizuDayGyzk.TryGetValue((date, jizu, gyzkId), out var ids) + ? ids : new List(); + + decimal jianliangNum = 0m, siliangDen = 0m; + double junfangchaSum = 0, zongchaSum = 0; + int junfangchaCnt = 0; + decimal erduSum = 0m, sanduSum = 0m; + decimal qjTdc = 0m, qjDc = 0m, qjCj = 0m; + decimal jj85 = 0m, jj80 = 0m, jj75 = 0m, jj70 = 0m; + decimal pianshuSum = 0m, qingjieDeduct = 0m, jiejingDeduct = 0m; + + foreach (var lid in lsjlIds) + { + var chetais = ctx.CdsChetai.TryGetValue(lid, out var ct) ? ct : new List(); + var cdsSum = ctx.CdsLsjl.TryGetValue(lid, out var cs) ? cs : null; + var xdjy = ctx.XdjyByLsjl.TryGetValue(lid, out var xd) ? xd : new List(); + var hbjy = ctx.HbjyByLsjl.TryGetValue(lid, out var hb) ? hb : new List(); + + foreach (var ctItem in chetais) + { + if (ctItem.Chetaihao < k.ChetaiStart || ctItem.Chetaihao > k.ChetaiEnd) continue; + var rate = ChetaiRate(ctx, date, jizu, ctItem.Chetaihao, k); + + // 缫折:茧量只记在落丝层,按「车台丝量占落丝丝量的比例」摊到车台,再看台比例摊到人 + var lsjlSiliang = cdsSum?.Siliang ?? 0; + var lsjlJianliang = cdsSum?.Jianliang ?? 0; + var chetaiShare = lsjlSiliang > 0 ? (decimal)ctItem.Siliang / lsjlSiliang : 0m; + jianliangNum += lsjlJianliang * chetaiShare * rate; + siliangDen += ctItem.Siliang * rate; + + // 中心纤度 / 野纤(纤度检验明细) + foreach (var x in xdjy.Where(x => x.Chetaihao == ctItem.Chetaihao)) + { + var xds = new[] { x.Xd1, x.Xd2, x.Xd3, x.Xd4 }.Where(v => v > 0).ToArray(); + if (guigeCfg != null) + { + q.CenterNum += xds.Count(v => Math.Abs(v - guigeCfg.CenterXiandu) < 0.0001) * rate; + q.YexianNum += xds.Count(v => v <= guigeCfg.XiaoyeLolimit || v >= guigeCfg.XiaoyeUplimit) * rate; + } + if (xds.Length > 0) + { + var avg = xds.Average(); + var jfc = Math.Sqrt(xds.Sum(v => (v - avg) * (v - avg)) / xds.Length); + junfangchaSum += jfc; junfangchaCnt++; + var n = Math.Max(1, (int)Math.Ceiling(xds.Length * 0.02)); + var sorted = xds.OrderBy(v => v).ToArray(); + var minAvg = sorted.Take(n).Average(); + var maxAvg = sorted.Skip(xds.Length - n).Average(); + zongchaSum += Math.Max(Math.Abs(minAvg - avg), Math.Abs(maxAvg - avg)); + } + } + + // 黑板检验:二度/三度/清洁/洁净 + foreach (var h in hbjy.Where(h => h.Chetaihao == ctItem.Chetaihao)) + { + erduSum += h.YdErdu * rate; + sanduSum += h.YdSandu * rate; + qjTdc += h.QjZycdTdc * rate; + qjDc += h.QjCycdDc * rate; + qjCj += h.QjPtcdCj * rate; + jj85 += h.Jj85 * rate; jj80 += h.Jj80 * rate; + jj75 += h.Jj75 * rate; jj70 += h.Jj70 * rate; + + pianshuSum += h.Pianshu * rate; + qingjieDeduct += (h.QjZycdTdc * 1.0m + + (h.QjCycdFs + h.QjCycdDc + h.QjCycdNfc + h.QjCycdDcj + h.QjCycdZlx) * 0.4m + + (h.QjPtcdXc + h.QjPtcdCj + h.QjPtcdLx + h.QjPtcdHjls) * 0.1m) * rate; + var jjCounts = new[] { h.Jj95, h.Jj90, h.Jj85, h.Jj80, h.Jj75, h.Jj70, h.Jj65, h.Jj60 }; + for (var i = 0; i < jjCounts.Length; i++) jiejingDeduct += jjCounts[i] * (i + 1) * rate; + } + } + } + + q.Saozhe = siliangDen > 0 ? Math.Round(jianliangNum * 100m / siliangDen, 0, MidpointRounding.AwayFromZero) + : cfg.SaoZhe; + q.Erdu = Math.Round(erduSum * half, 2, MidpointRounding.AwayFromZero); + q.Sandu = Math.Round(sanduSum * half, 2, MidpointRounding.AwayFromZero); + q.CenterNum = Math.Round(q.CenterNum * half, 2, MidpointRounding.AwayFromZero); + q.YexianNum = Math.Round(q.YexianNum * half, 2, MidpointRounding.AwayFromZero); + q.QingjiePianshu = Math.Round((qjTdc + qjDc + qjCj) * half, 2, MidpointRounding.AwayFromZero); + q.JiejingPianshu = Math.Round((jj85 + jj80 + jj75 + jj70) * half, 2, MidpointRounding.AwayFromZero); + q.QingjieKouKuan = (qjTdc * 2m + qjDc * 0.5m + qjCj * 2m) * half; + q.JiejingKouKuan = (jj85 * 0.2m + jj80 * 1m + jj75 * 2m + jj70 * 5m) * half; + q.YexianKouKuan = CalYeXianKouKuan(ctx, cfg.Guige, q.YexianNum); + + q.Junfangcha = junfangchaCnt > 0 ? Math.Round(junfangchaSum / junfangchaCnt, 2) : 0; + q.Zongcha = junfangchaCnt > 0 ? Math.Round(zongchaSum / junfangchaCnt, 2) : 0; + q.QingjieFen = pianshuSum > 0 + ? Math.Round((pianshuSum - qingjieDeduct) * 100m / pianshuSum, 2, MidpointRounding.AwayFromZero) : 100m; + q.JiejingFen = pianshuSum > 0 + ? Math.Round((pianshuSum * 100m - jiejingDeduct * 5m) / pianshuSum, 2, MidpointRounding.AwayFromZero) : 100m; + + return q; + } + + /// 车台看台比例 = 本人该台工时 / 该台所有看台人工时之和 + private decimal ChetaiRate(CalcContext ctx, DateTime date, int jizu, int chetaihao, KtjlDcg k) + { + var same = ctx.DcgKtjl.Where(x => x.Date.Date == date && x.Jizu == jizu && x.Banci == k.Banci + && x.ChetaiStart <= chetaihao && x.ChetaiEnd >= chetaihao).ToList(); + var sum = same.Sum(x => x.Hours); + return sum > 0 ? k.Hours / sum : 0m; + } + + /// 野纤扣款:按规格取挡车纤度奖扣表,≤2 条取扣款最大的 2 条求和 + private decimal CalYeXianKouKuan(CalcContext ctx, string guige, decimal yexianNum) + { + if (yexianNum <= 0) return 0m; + var list = ctx.YeXianJiangKou(guige); + if (list.Count == 0) return 0m; + var items = ExpandCount(yexianNum, list); + if (items.Count >= 2) return items.OrderByDescending(i => i.KouKuan).Take(2).Sum(i => i.KouKuan); + return items.Sum(i => i.KouKuan); + } + + /// 按数量展开:整数部分逐条取,小数部分按最后一条比例折算 + private static List ExpandCount(decimal count, List list) + { + var result = new List(); + var intPart = (int)Math.Floor(count); + var frac = count - intPart; + for (var i = 0; i < intPart; i++) result.Add(list[i % list.Count]); + if (frac > 0) result.Add(list[intPart % list.Count]); + return result; + } + + // ==================== 内部:等级 ==================== + + /// 挡车工等级:木桶短板法,六项各自定级后取最低分 + private (string Dengji, decimal Dengjifen) CalDengJi(CalcContext ctx, string guige, double piancha, + double dacha, decimal qingjie, decimal jiejing, decimal erdu, decimal sandu, ConfGyzk cfg) + { + var zbs = ctx.Djzb.Where(z => z.Guige == guige).ToList(); + if (zbs.Count == 0) return (cfg.QsDengji, cfg.QsDengjifen); + + var candidates = new List(); + AddBest(candidates, zbs.Where(z => z.Piancha >= piancha)); + AddBest(candidates, zbs.Where(z => z.Dacha >= dacha)); + AddBest(candidates, zbs.Where(z => z.Qingjie <= (double)qingjie)); + AddBest(candidates, zbs.Where(z => z.Jiejing <= (double)jiejing)); + AddBest(candidates, zbs.Where(z => z.Erdu >= (double)erdu)); + AddBest(candidates, zbs.Where(z => z.Sandu >= (double)sandu)); + + if (candidates.Count == 0) return (cfg.QsDengji, cfg.QsDengjifen); + var worst = candidates.OrderBy(z => z.GradeScore).First(); + return (worst.Grade, (decimal)worst.GradeScore); + } + + private static void AddBest(List target, IEnumerable src) + { + var best = src.OrderByDescending(z => z.GradeScore).FirstOrDefault(); + if (best != null) target.Add(best); + } + + /// 车头工等级:按挡车工丝量加权平均等级分后回落取整档 + private (string Dengji, decimal Dengjifen) CalCtgDengji(CalcContext ctx, List dcgRows, + DateTime date, int jizu, long gyzkId, string guige, ConfGyzk cfg) + { + var related = dcgRows.Where(x => x.Date.Date == date && x.Jizu == jizu && x.GyzkId == gyzkId).ToList(); + var siliangSum = related.Sum(x => x.ClSiliang); + if (siliangSum <= 0) return (cfg.QsDengji, cfg.QsDengjifen); + + var weighted = related.Sum(x => x.ClSiliang * x.Dengjifen) / siliangSum; + var zbs = ctx.Djzb.Where(z => z.Guige == guige && (decimal)z.GradeScore <= weighted) + .OrderByDescending(z => z.GradeScore).FirstOrDefault(); + return zbs != null ? (zbs.Grade, (decimal)zbs.GradeScore) : (cfg.QsDengji, cfg.QsDengjifen); + } + + // ==================== 内部:单价 / 金额 ==================== + + /// 按绪数档取岗位基数金额:<60→50档,<70→60档,<100→70档,否则 100 档 + private static decimal GetDcgDanJiaBase(decimal xushu, ConfDcg conf) + { + if (xushu < 60m) return conf.JineJisuanjishu50; + if (xushu < 70m) return conf.JineJisuanjishu60; + if (xushu < 100m) return conf.JineJisuanjishu70; + return conf.JineJisuanjishu100; + } + + /// 缫折考核:±2kg 死区不奖不扣,超出部分按封顶 clamp,每 1kg 每 100kg 丝 奖扣 price 元 + private static decimal CalJineSaozhe(decimal saoZheCfg, decimal saozhe, decimal siliang, int cap, decimal price) + { + if (Math.Abs(saoZheCfg - saozhe) <= SaozheDeadZone) return 0m; + var diff = saoZheCfg - saozhe; + diff = diff < 0 ? diff + SaozheDeadZone : diff - SaozheDeadZone; + if (diff > cap) diff = cap; + if (diff < -cap) diff = -cap; + return Math.Round(diff * (siliang / 1000m / 100m) * price, 2, MidpointRounding.AwayFromZero); + } + + private static string BanciName(int banci) => banci switch { 1 => "甲", 2 => "乙", _ => banci.ToString() }; +} diff --git a/backend/F9WebAiTest.Application/Services/PayRoll/ConfEmployeeService.cs b/backend/F9WebAiTest.Application/Services/PayRoll/ConfEmployeeService.cs new file mode 100644 index 0000000..222afa6 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/PayRoll/ConfEmployeeService.cs @@ -0,0 +1,225 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.PayRoll; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using SysEmp = F9WebAiTest.Domain.Entities.SYS.Employees; +using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes; + +namespace F9WebAiTest.Application.Services.PayRoll; + +/// 薪资配置-账期(CONFZQ):考勤统计的「前缫出勤天数」来源 +public interface IConfZqService +{ + Task> GetListAsync(int? year); + Task GetByYearMonthAsync(int year, int month); + Task SaveAsync(ConfZqDto dto); + Task DeleteAsync(long id); +} + +/// 薪资配置-职工(CONFEMPLOYEE):法定假小时数 / 是否学工 +public interface IConfEmployeeService +{ + Task> GetListAsync(long? employeeId, long? worktypeId); + Task SaveAsync(ConfEmployeeDto dto); + Task DeleteAsync(long id); +} + +/// 岗位胜任考核:考核结果决定加班费单价档位 +public interface IGwsrkhService +{ + Task> GetListAsync(int year, int month, long depId); + Task SaveAsync(GwsrkhDto dto); + Task DeleteAsync(long id); +} + +public class ConfZqService : IConfZqService +{ + private readonly IRepository _repo; + public ConfZqService(IRepository repo) => _repo = repo; + + public async Task> GetListAsync(int? year) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (year.HasValue) q = q.Where(x => x.Year == year.Value); + return q.ToList().OrderByDescending(x => x.Year).ThenByDescending(x => x.Month) + .Select(x => new ConfZqDto + { + Id = x.Id, Year = x.Year, Month = x.Month, + QsQuanqinDays = x.QsQuanqinDays, Memo = x.Memo ?? "" + }).ToList(); + } + + public async Task GetByYearMonthAsync(int year, int month) + { + var hit = await _repo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + return hit == null + ? new ConfZqDto { Year = year, Month = month } + : new ConfZqDto { Id = hit.Id, Year = hit.Year, Month = hit.Month, QsQuanqinDays = hit.QsQuanqinDays, Memo = hit.Memo ?? "" }; + } + + public async Task SaveAsync(ConfZqDto dto) + { + if (dto.Year <= 0 || dto.Month <= 0) throw new ServiceException("请填写年度与月份"); + var exist = await _repo.FirstOrDefaultAsync(x => x.Year == dto.Year && x.Month == dto.Month && x.Id != dto.Id); + if (exist != null) throw new ServiceException($"{dto.Year} 年 {dto.Month} 月的账期薪资配置已存在"); + + ConfZq entity; + if (dto.Id > 0) + { + entity = await _repo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("配置不存在", 404); + } + else entity = new ConfZq(); + + entity.Year = dto.Year; + entity.Month = dto.Month; + entity.QsQuanqinDays = dto.QsQuanqinDays; + entity.Memo = dto.Memo ?? ""; + + if (dto.Id > 0) await _repo.UpdateAsync(entity); else await _repo.AddAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("配置不存在", 404); + await _repo.DeleteAsync(e); + } +} + +public class ConfEmployeeService : IConfEmployeeService +{ + private readonly IRepository _repo; + private readonly IRepository _empRepo; + private readonly IRepository _wtRepo; + + public ConfEmployeeService(IRepository repo, IRepository empRepo, + IRepository wtRepo) + { + _repo = repo; _empRepo = empRepo; _wtRepo = wtRepo; + } + + public async Task> GetListAsync(long? employeeId, long? worktypeId) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (employeeId.HasValue && employeeId.Value > 0) q = q.Where(x => x.EmployeeId == employeeId.Value); + if (worktypeId.HasValue && worktypeId.Value > 0) q = q.Where(x => x.WorktypeId == worktypeId.Value); + var emps = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id); + var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName); + return q.ToList().OrderBy(x => x.EmployeeId).Select(x => new ConfEmployeeDto + { + Id = x.Id, + EmployeeId = x.EmployeeId, + EmployeeCode = emps.TryGetValue(x.EmployeeId, out var e) ? (e.Code ?? "") : "", + EmployeeName = emps.TryGetValue(x.EmployeeId, out var e2) ? (e2.Name ?? "") : "", + WorktypeId = x.WorktypeId, + WorktypeName = wts.TryGetValue(x.WorktypeId, out var wn) ? wn : "", + HoursFadingBz = x.HoursFadingBz, + HoursFadingGz = x.HoursFadingGz, + IsXuegong = x.IsXuegong, + Memo = x.Memo ?? "" + }).ToList(); + } + + public async Task SaveAsync(ConfEmployeeDto dto) + { + if (dto.EmployeeId <= 0) throw new ServiceException("请选择职工"); + var exist = await _repo.FirstOrDefaultAsync(x => x.EmployeeId == dto.EmployeeId && x.WorktypeId == dto.WorktypeId && x.Id != dto.Id); + if (exist != null) throw new ServiceException("该职工该工种的薪资配置已存在"); + + ConfEmployee entity; + if (dto.Id > 0) + { + entity = await _repo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("配置不存在", 404); + } + else entity = new ConfEmployee(); + + entity.EmployeeId = dto.EmployeeId; + entity.WorktypeId = dto.WorktypeId; + entity.HoursFadingBz = dto.HoursFadingBz; + entity.HoursFadingGz = dto.HoursFadingGz; + entity.IsXuegong = dto.IsXuegong; + entity.Memo = dto.Memo ?? ""; + + if (dto.Id > 0) await _repo.UpdateAsync(entity); else await _repo.AddAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("配置不存在", 404); + await _repo.DeleteAsync(e); + } +} + +public class GwsrkhService : IGwsrkhService +{ + private readonly IRepository _repo; + private readonly IRepository _depRepo; + private readonly IRepository _empRepo; + + public GwsrkhService(IRepository repo, IRepository depRepo, + IRepository empRepo) + { + _repo = repo; _depRepo = depRepo; _empRepo = empRepo; + } + + public async Task> GetListAsync(int year, int month, long depId) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable().Where(x => x.Year == year && x.Month == month); + if (depId > 0) q = q.Where(x => x.DepId == depId); + var depts = (await _depRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name); + var emps = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id); + return q.ToList().OrderBy(x => x.EmployeeId).Select(x => new GwsrkhDto + { + Id = x.Id, Year = x.Year, Month = x.Month, + DepId = x.DepId, DepName = depts.TryGetValue(x.DepId, out var dn) ? dn : "", + EmployeeId = x.EmployeeId, + EmployeeCode = emps.TryGetValue(x.EmployeeId, out var e) ? (e.Code ?? "") : "", + EmployeeName = emps.TryGetValue(x.EmployeeId, out var e2) ? (e2.Name ?? "") : "", + KhJieguo = x.KhJieguo ?? "", + Memo = x.Memo ?? "" + }).ToList(); + } + + public async Task SaveAsync(GwsrkhDto dto) + { + if (dto.EmployeeId <= 0) throw new ServiceException("请选择职工"); + if (string.IsNullOrWhiteSpace(dto.KhJieguo)) throw new ServiceException("请选择考核结果"); + var exist = await _repo.FirstOrDefaultAsync(x => x.Year == dto.Year && x.Month == dto.Month + && x.EmployeeId == dto.EmployeeId && x.Id != dto.Id); + if (exist != null) throw new ServiceException("该职工本月考核结果已存在"); + + Gwsrkh entity; + if (dto.Id > 0) + { + entity = await _repo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("记录不存在", 404); + } + else entity = new Gwsrkh(); + + entity.Year = dto.Year; + entity.Month = dto.Month; + entity.DepId = dto.DepId; + entity.EmployeeId = dto.EmployeeId; + entity.KhJieguo = dto.KhJieguo.Trim(); + entity.Memo = dto.Memo ?? ""; + + if (dto.Id > 0) await _repo.UpdateAsync(entity); else await _repo.AddAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("记录不存在", 404); + await _repo.DeleteAsync(e); + } +} diff --git a/backend/F9WebAiTest.Application/Services/PayRoll/PayRollConfService.cs b/backend/F9WebAiTest.Application/Services/PayRoll/PayRollConfService.cs new file mode 100644 index 0000000..28d263f --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/PayRoll/PayRollConfService.cs @@ -0,0 +1,493 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.PayRoll; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.PayRoll; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes; + +namespace F9WebAiTest.Application.Services.PayRoll; + +// 薪资计算依赖的 5 套配置(对应 F9 PAYROLL 下的 CONFGYZK / CONFGZ / CONFDCG / DCXDJK / XinZiJiaGePeiZhi) + +// ==================== 薪资配置-工艺庄口 ==================== + +public interface IConfGyzkService +{ + Task> GetListAsync(int? year, int? month); + Task GetForEditAsync(long id); + Task SaveAsync(ConfGyzkSaveDto dto); + Task DeleteAsync(long id); +} + +public class ConfGyzkService : IConfGyzkService +{ + private readonly IRepository _repo; + private readonly IRepository _zkRepo; + + public ConfGyzkService(IRepository repo, IRepository zkRepo) + { + _repo = repo; + _zkRepo = zkRepo; + } + + public async Task> GetListAsync(int? year, int? month) + { + var all = await _repo.GetAllAsync(); + var q = all.AsQueryable(); + if (year.HasValue) q = q.Where(x => x.Year == year.Value); + if (month.HasValue) q = q.Where(x => x.Month == month.Value); + var items = q.OrderByDescending(x => x.Year).ThenByDescending(x => x.Month).ThenBy(x => x.GyzkId).ToList(); + + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + + return items.Select(x => new ConfGyzkListItemDto + { + Id = x.Id, Year = x.Year, Month = x.Month, GyzkId = x.GyzkId, + GyzkName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Guige = x.Guige, QsTaichan = x.QsTaichan, QsDengji = x.QsDengji, QsDengjifen = x.QsDengjifen, + SaoZhe = x.SaoZhe, MeiTongJianLiang = x.MeiTongJianLiang, LuoSiTongShu = x.LuoSiTongShu, + MeiCheSiLiang = x.MeiCheSiLiang, DcgJijianXishu = x.DcgJijianXishu, CtgJijianXishu = x.CtgJijianXishu, + GyzkLtwxKs = x.GyzkLtwxKs, GyzkLtwxl = x.GyzkLtwxl, XjChanliangPrice = x.XjChanliangPrice + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工艺庄口薪资配置不存在或已删除", 404); + return new ConfGyzkSaveDto + { + Id = e.Id, Year = e.Year, Month = e.Month, GyzkId = e.GyzkId, Guige = e.Guige, + QsTaichan = e.QsTaichan, QsDengji = e.QsDengji, QsDengjifen = e.QsDengjifen, SaoZhe = e.SaoZhe, + MeiTongJianLiang = e.MeiTongJianLiang, LuoSiTongShu = e.LuoSiTongShu, MeiCheSiLiang = e.MeiCheSiLiang, + DcgJijianXishu = e.DcgJijianXishu, CtgJijianXishu = e.CtgJijianXishu, + GyzkLtwxKs = e.GyzkLtwxKs, GyzkLtwxl = e.GyzkLtwxl, XjChanliangPrice = e.XjChanliangPrice + }; + } + + public async Task SaveAsync(ConfGyzkSaveDto dto) + { + if (dto.GyzkId == 0) throw new ServiceException("请选择工艺庄口"); + if (dto.Year <= 0 || dto.Month <= 0) throw new ServiceException("年度/月份无效"); + + ConfGyzk e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("工艺庄口薪资配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Year == dto.Year + && x.Month == dto.Month && x.GyzkId == dto.GyzkId); + if (dup != null) throw new ServiceException("该年度月份已存在此庄口的配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.Year == dto.Year && x.Month == dto.Month && x.GyzkId == dto.GyzkId); + if (dup) throw new ServiceException("该年度月份已存在此庄口的配置!"); + e = new ConfGyzk(); + await _repo.AddAsync(e); + } + + e.Year = dto.Year; + e.Month = dto.Month; + e.GyzkId = dto.GyzkId; + e.Guige = dto.Guige ?? ""; + e.QsTaichan = dto.QsTaichan; + e.QsDengji = dto.QsDengji ?? ""; + e.QsDengjifen = dto.QsDengjifen; + e.SaoZhe = dto.SaoZhe; + e.MeiTongJianLiang = dto.MeiTongJianLiang; + e.LuoSiTongShu = dto.LuoSiTongShu; + e.MeiCheSiLiang = dto.MeiCheSiLiang; + e.DcgJijianXishu = dto.DcgJijianXishu; + e.CtgJijianXishu = dto.CtgJijianXishu; + e.GyzkLtwxKs = dto.GyzkLtwxKs; + e.GyzkLtwxl = dto.GyzkLtwxl; + e.XjChanliangPrice = dto.XjChanliangPrice; + + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工艺庄口薪资配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } +} + +// ==================== 薪资配置-工种 ==================== + +public interface IConfGzService +{ + Task> GetListAsync(); + Task GetForEditAsync(long id); + Task SaveAsync(ConfGzSaveDto dto); + Task DeleteAsync(long id); +} + +public class ConfGzService : IConfGzService +{ + private readonly IRepository _repo; + private readonly IRepository _wtRepo; + + public ConfGzService(IRepository repo, IRepository wtRepo) + { + _repo = repo; + _wtRepo = wtRepo; + } + + public async Task> GetListAsync() + { + var items = await _repo.GetAllAsync(); + var wts = await _wtRepo.GetAllAsync(); + var wtDict = wts.ToDictionary(w => w.Id, w => w.WorkName); + return items.Select(x => ToDto(x, wtDict)).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工种薪资配置不存在或已删除", 404); + return FromEntity(e); + } + + public async Task SaveAsync(ConfGzSaveDto dto) + { + if (dto.WorkTypeId == 0) throw new ServiceException("请选择工种"); + + ConfGz e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("工种薪资配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.WorkTypeId == dto.WorkTypeId); + if (dup != null) throw new ServiceException("该工种已存在薪资配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.WorkTypeId == dto.WorkTypeId); + if (dup) throw new ServiceException("该工种已存在薪资配置!"); + e = new ConfGz(); + await _repo.AddAsync(e); + } + Apply(e, dto); + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("工种薪资配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } + + private static ConfGzListItemDto ToDto(ConfGz x, Dictionary wtDict) => new() + { + Id = x.Id, WorkTypeId = x.WorkTypeId, WorkTypeName = wtDict.GetValueOrDefault(x.WorkTypeId, ""), + HoursPerday = x.HoursPerday, QqDaysStandard = x.QqDaysStandard, QqHoursMaxQj = x.QqHoursMaxQj, + QqHoursDkMinjiaban = x.QqHoursDkMinjiaban, QqHoursDkQingjia = x.QqHoursDkQingjia, + XgJineQuanqin = x.XgJineQuanqin, XgJineGangwei = x.XgJineGangwei, JineNiangong = x.JineNiangong, + JineQuanqin = x.JineQuanqin, JineGangwei = x.JineGangwei, JineYuekhJixiao = x.JineYuekhJixiao, + JineGwkh = x.JineGwkh, JineYueticheng = x.JineYueticheng, JineJtkhYear = x.JineJtkhYear, + JineJisuanjishu = x.JineJisuanjishu, JineYeban = x.JineYeban, JineJiaotongButie = x.JineJiaotongButie, + LiudongJineGuding = x.LiudongJineGuding, JiaqiJineKuangongKk = x.JiaqiJineKuangongKk, + JiaqiJineFadingGz = x.JiaqiJineFadingGz, JiaqiJineFadingBz = x.JiaqiJineFadingBz, + JiaqiJineHunjiaBz = x.JiaqiJineHunjiaBz, JiaqiJineSangjiaBz = x.JiaqiJineSangjiaBz, + JiaqiJineBingjiaBz = x.JiaqiJineBingjiaBz, JiaqiJineLunxiuBz = x.JiaqiJineLunxiuBz, + JiabanJineBz = x.JiabanJineBz, Jiaqi_Jine_TiaoxiuBz = x.Jiaqi_Jine_TiaoxiuBz, + Jiaqi_Jine_NianjiaBz = x.Jiaqi_Jine_NianjiaBz, GwsrkhJineQianhege = x.GwsrkhJineQianhege, + GwsrkhJineQiwang = x.GwsrkhJineQiwang, GwsrkhJineHege = x.GwsrkhJineHege, + GwsrkhJineShengren = x.GwsrkhJineShengren, GwsrkhJineChaoshengren = x.GwsrkhJineChaoshengren, + Jiaban_GwsrkhJineQianhege = x.Jiaban_GwsrkhJineQianhege, Jiaban_GwsrkhJineQiwang = x.Jiaban_GwsrkhJineQiwang, + Jiaban_GwsrkhJineHege = x.Jiaban_GwsrkhJineHege, Jiaban_GwsrkhJineShengren = x.Jiaban_GwsrkhJineShengren, + Jiaban_GwsrkhJineChaoshengren = x.Jiaban_GwsrkhJineChaoshengren + }; + + private static ConfGzSaveDto FromEntity(ConfGz x) => new() + { + Id = x.Id, WorkTypeId = x.WorkTypeId, HoursPerday = x.HoursPerday, QqDaysStandard = x.QqDaysStandard, + QqHoursMaxQj = x.QqHoursMaxQj, QqHoursDkMinjiaban = x.QqHoursDkMinjiaban, QqHoursDkQingjia = x.QqHoursDkQingjia, + XgJineQuanqin = x.XgJineQuanqin, XgJineGangwei = x.XgJineGangwei, JineNiangong = x.JineNiangong, + JineQuanqin = x.JineQuanqin, JineGangwei = x.JineGangwei, JineYuekhJixiao = x.JineYuekhJixiao, + JineGwkh = x.JineGwkh, JineYueticheng = x.JineYueticheng, JineJtkhYear = x.JineJtkhYear, + JineJisuanjishu = x.JineJisuanjishu, JineYeban = x.JineYeban, JineJiaotongButie = x.JineJiaotongButie, + LiudongJineGuding = x.LiudongJineGuding, JiaqiJineKuangongKk = x.JiaqiJineKuangongKk, + JiaqiJineFadingGz = x.JiaqiJineFadingGz, JiaqiJineFadingBz = x.JiaqiJineFadingBz, + JiaqiJineHunjiaBz = x.JiaqiJineHunjiaBz, JiaqiJineSangjiaBz = x.JiaqiJineSangjiaBz, + JiaqiJineBingjiaBz = x.JiaqiJineBingjiaBz, JiaqiJineLunxiuBz = x.JiaqiJineLunxiuBz, + JiabanJineBz = x.JiabanJineBz, Jiaqi_Jine_TiaoxiuBz = x.Jiaqi_Jine_TiaoxiuBz, + Jiaqi_Jine_NianjiaBz = x.Jiaqi_Jine_NianjiaBz, GwsrkhJineQianhege = x.GwsrkhJineQianhege, + GwsrkhJineQiwang = x.GwsrkhJineQiwang, GwsrkhJineHege = x.GwsrkhJineHege, + GwsrkhJineShengren = x.GwsrkhJineShengren, GwsrkhJineChaoshengren = x.GwsrkhJineChaoshengren, + Jiaban_GwsrkhJineQianhege = x.Jiaban_GwsrkhJineQianhege, Jiaban_GwsrkhJineQiwang = x.Jiaban_GwsrkhJineQiwang, + Jiaban_GwsrkhJineHege = x.Jiaban_GwsrkhJineHege, Jiaban_GwsrkhJineShengren = x.Jiaban_GwsrkhJineShengren, + Jiaban_GwsrkhJineChaoshengren = x.Jiaban_GwsrkhJineChaoshengren + }; + + private static void Apply(ConfGz e, ConfGzSaveDto d) + { + e.WorkTypeId = d.WorkTypeId; + e.HoursPerday = d.HoursPerday; e.QqDaysStandard = d.QqDaysStandard; + e.QqHoursMaxQj = d.QqHoursMaxQj; e.QqHoursDkMinjiaban = d.QqHoursDkMinjiaban; + e.QqHoursDkQingjia = d.QqHoursDkQingjia; e.XgJineQuanqin = d.XgJineQuanqin; + e.XgJineGangwei = d.XgJineGangwei; e.JineNiangong = d.JineNiangong; + e.JineQuanqin = d.JineQuanqin; e.JineGangwei = d.JineGangwei; + e.JineYuekhJixiao = d.JineYuekhJixiao; e.JineGwkh = d.JineGwkh; + e.JineYueticheng = d.JineYueticheng; e.JineJtkhYear = d.JineJtkhYear; + e.JineJisuanjishu = d.JineJisuanjishu; e.JineYeban = d.JineYeban; + e.JineJiaotongButie = d.JineJiaotongButie; e.LiudongJineGuding = d.LiudongJineGuding; + e.JiaqiJineKuangongKk = d.JiaqiJineKuangongKk; e.JiaqiJineFadingGz = d.JiaqiJineFadingGz; + e.JiaqiJineFadingBz = d.JiaqiJineFadingBz; e.JiaqiJineHunjiaBz = d.JiaqiJineHunjiaBz; + e.JiaqiJineSangjiaBz = d.JiaqiJineSangjiaBz; e.JiaqiJineBingjiaBz = d.JiaqiJineBingjiaBz; + e.JiaqiJineLunxiuBz = d.JiaqiJineLunxiuBz; e.JiabanJineBz = d.JiabanJineBz; + e.Jiaqi_Jine_TiaoxiuBz = d.Jiaqi_Jine_TiaoxiuBz; e.Jiaqi_Jine_NianjiaBz = d.Jiaqi_Jine_NianjiaBz; + e.GwsrkhJineQianhege = d.GwsrkhJineQianhege; e.GwsrkhJineQiwang = d.GwsrkhJineQiwang; + e.GwsrkhJineHege = d.GwsrkhJineHege; e.GwsrkhJineShengren = d.GwsrkhJineShengren; + e.GwsrkhJineChaoshengren = d.GwsrkhJineChaoshengren; + e.Jiaban_GwsrkhJineQianhege = d.Jiaban_GwsrkhJineQianhege; + e.Jiaban_GwsrkhJineQiwang = d.Jiaban_GwsrkhJineQiwang; + e.Jiaban_GwsrkhJineHege = d.Jiaban_GwsrkhJineHege; + e.Jiaban_GwsrkhJineShengren = d.Jiaban_GwsrkhJineShengren; + e.Jiaban_GwsrkhJineChaoshengren = d.Jiaban_GwsrkhJineChaoshengren; + } +} + +// ==================== 薪资配置-挡车/车头岗位 ==================== + +public interface IConfDcgService +{ + Task> GetListAsync(); + Task GetForEditAsync(long id); + Task SaveAsync(ConfDcgSaveDto dto); + Task DeleteAsync(long id); +} + +public class ConfDcgService : IConfDcgService +{ + private readonly IRepository _repo; + private readonly IRepository _depRepo; + + public ConfDcgService(IRepository repo, IRepository depRepo) + { + _repo = repo; + _depRepo = depRepo; + } + + public async Task> GetListAsync() + { + var items = await _repo.GetAllAsync(); + var deps = await _depRepo.GetAllAsync(); + var depDict = deps.ToDictionary(d => d.Id, d => d.Name); + return items.Select(x => new ConfDcgListItemDto + { + Id = x.Id, DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + JineJisuanjishu50 = x.JineJisuanjishu50, JineJisuanjishu60 = x.JineJisuanjishu60, + JineJisuanjishu70 = x.JineJisuanjishu70, JineJisuanjishu100 = x.JineJisuanjishu100, + JineCtg = x.JineCtg + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车岗位薪资配置不存在或已删除", 404); + return new ConfDcgSaveDto + { + Id = e.Id, DepId = e.DepId, JineJisuanjishu50 = e.JineJisuanjishu50, + JineJisuanjishu60 = e.JineJisuanjishu60, JineJisuanjishu70 = e.JineJisuanjishu70, + JineJisuanjishu100 = e.JineJisuanjishu100, JineCtg = e.JineCtg + }; + } + + public async Task SaveAsync(ConfDcgSaveDto dto) + { + if (dto.DepId == 0) throw new ServiceException("请选择部门"); + + ConfDcg e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("挡车岗位薪资配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.DepId == dto.DepId); + if (dup != null) throw new ServiceException("该部门已存在挡车岗位薪资配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.DepId == dto.DepId); + if (dup) throw new ServiceException("该部门已存在挡车岗位薪资配置!"); + e = new ConfDcg(); + await _repo.AddAsync(e); + } + e.DepId = dto.DepId; + e.JineJisuanjishu50 = dto.JineJisuanjishu50; + e.JineJisuanjishu60 = dto.JineJisuanjishu60; + e.JineJisuanjishu70 = dto.JineJisuanjishu70; + e.JineJisuanjishu100 = dto.JineJisuanjishu100; + e.JineCtg = dto.JineCtg; + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车岗位薪资配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } +} + +// ==================== 挡车纤度奖扣 ==================== + +public interface IDcxdjkService +{ + Task> GetListAsync(string guige); + Task GetForEditAsync(long id); + Task SaveAsync(DcxdjkSaveDto dto); + Task DeleteAsync(long id); +} + +public class DcxdjkService : IDcxdjkService +{ + private readonly IRepository _repo; + + public DcxdjkService(IRepository repo) => _repo = repo; + + public async Task> GetListAsync(string guige) + { + var items = await _repo.GetAllAsync(); + var q = items.AsQueryable(); + if (!string.IsNullOrWhiteSpace(guige)) q = q.Where(x => x.GuiGe == guige); + return q.OrderBy(x => x.GuiGe).ThenBy(x => x.XianDu).ToList() + .Select(x => new DcxdjkListItemDto + { + Id = x.Id, GuiGe = x.GuiGe, XianDu = x.XianDu, JiangLi = x.JiangLi, KouKuan = x.KouKuan + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车纤度奖扣不存在或已删除", 404); + return new DcxdjkSaveDto { Id = e.Id, GuiGe = e.GuiGe, XianDu = e.XianDu, JiangLi = e.JiangLi, KouKuan = e.KouKuan }; + } + + public async Task SaveAsync(DcxdjkSaveDto dto) + { + if (string.IsNullOrWhiteSpace(dto.GuiGe)) throw new ServiceException("请填写规格"); + + DangCheXianDuJiangKou e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("挡车纤度奖扣不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.GuiGe == dto.GuiGe && x.XianDu == dto.XianDu); + if (dup != null) throw new ServiceException("该规格已存在相同纤度的奖扣配置!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.GuiGe == dto.GuiGe && x.XianDu == dto.XianDu); + if (dup) throw new ServiceException("该规格已存在相同纤度的奖扣配置!"); + e = new DangCheXianDuJiangKou(); + await _repo.AddAsync(e); + } + e.GuiGe = dto.GuiGe.Trim(); + e.XianDu = dto.XianDu; + e.JiangLi = dto.JiangLi; + e.KouKuan = dto.KouKuan; + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("挡车纤度奖扣不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } +} + +// ==================== 薪资价格配置 ==================== + +public interface IXzjgService +{ + Task> GetListAsync(string group); + Task GetForEditAsync(long id); + Task SaveAsync(XzjgSaveDto dto); + Task DeleteAsync(long id); + Task GetPriceAsync(string group, string key); +} + +public class XzjgService : IXzjgService +{ + private readonly IRepository _repo; + + public XzjgService(IRepository repo) => _repo = repo; + + public async Task> GetListAsync(string group) + { + var items = await _repo.GetAllAsync(); + var q = items.AsQueryable(); + if (!string.IsNullOrWhiteSpace(group)) q = q.Where(x => x.Group == group); + return q.OrderBy(x => x.Group).ThenBy(x => x.Key).ToList() + .Select(x => new XzjgListItemDto + { + Id = x.Id, Group = x.Group, Key = x.Key, Unit = x.Unit, Price = x.Price, Memo = x.Memo + }).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("薪资价格配置不存在或已删除", 404); + return new XzjgSaveDto { Id = e.Id, Group = e.Group, Key = e.Key, Unit = e.Unit, Price = e.Price, Memo = e.Memo }; + } + + public async Task SaveAsync(XzjgSaveDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Group)) throw new ServiceException("请填写分组"); + if (string.IsNullOrWhiteSpace(dto.Key)) throw new ServiceException("请填写项目"); + var grp = dto.Group.Trim(); + var key = dto.Key.Trim(); + + XinZiJiaGePeiZhi e; + if (dto.Id > 0) + { + e = await _repo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("薪资价格配置不存在或已删除", 404); + var dup = await _repo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Group == grp && x.Key == key); + if (dup != null) throw new ServiceException("该分组已存在相同的项目!"); + } + else + { + var dup = await _repo.AnyAsync(x => x.Group == grp && x.Key == key); + if (dup) throw new ServiceException("该分组已存在相同的项目!"); + e = new XinZiJiaGePeiZhi(); + await _repo.AddAsync(e); + } + e.Group = grp; + e.Key = key; + e.Unit = dto.Unit ?? ""; + e.Price = dto.Price; + e.Memo = dto.Memo ?? ""; + await _repo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _repo.GetByIdAsync(id); + if (e == null) throw new ServiceException("薪资价格配置不存在或已删除", 404); + await _repo.HardDeleteAsync(e); + } + + /// 取价:无配置返回 0 + public async Task GetPriceAsync(string group, string key) + { + var e = await _repo.FirstOrDefaultAsync(x => x.Group == group && x.Key == key); + return e?.Price ?? 0m; + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/CdsService.cs b/backend/F9WebAiTest.Application/Services/Prod/CdsService.cs new file mode 100644 index 0000000..ef06a74 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/CdsService.cs @@ -0,0 +1,257 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 秤大丝:以落丝记录为主表,按车台录入 4 个丝量值并汇总把数/丝量/缫折/台产 +public interface ICdsService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId); + Task GetEditAsync(long lsjlId); + Task SaveAsync(CdsSaveDto dto); + Task DeleteAsync(long lsjlId); +} + +public class CdsService : ICdsService +{ + private readonly IRepository _lsjlRepo; + private readonly IRepository _cdsLsjlRepo; + private readonly IRepository _cdsChetaiRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + + public CdsService( + IRepository lsjlRepo, + IRepository cdsLsjlRepo, + IRepository cdsChetaiRepo, + IRepository deptRepo, + IRepository zkRepo) + { + _lsjlRepo = lsjlRepo; + _cdsLsjlRepo = cdsLsjlRepo; + _cdsChetaiRepo = cdsChetaiRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId) + { + var lsjls = await _lsjlRepo.GetAllAsync(); + var q = lsjls.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + var sums = (await _cdsLsjlRepo.GetAllAsync()).ToDictionary(x => x.LsjlId); + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.WorkDate).ThenBy(x => x.Jizu).ThenBy(x => x.Huishu) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sums.TryGetValue(x.Id, out var sum); + return new CdsListItemDto + { + LsjlId = x.Id, + WorkDate = x.WorkDate, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + Jizu = x.Jizu, + Huishu = x.Huishu, + Guige = x.Guige, + ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Yanse = x.Yanse, + Isbanche = x.Isbanche, + IsbancheName = x.Isbanche ? "半车" : "整车", + Chetaishu = x.Chetaishu, + ChetaiStart = x.ChetaiStart, + ChetaiEnd = x.ChetaiEnd, + Hours = x.Hours, + Tongshu = x.Tongshu, + Meitongjianliang = x.Meitongjianliang, + YingchengNum = YingchengNum(x), + IsWeighed = sum != null, + Num = sum?.Num ?? 0, + Siliang = sum?.Siliang ?? 0, + Jianliang = sum?.Jianliang ?? 0, + Saozhe = sum?.Saozhe ?? 0, + Taichan = sum?.Taichan ?? 0 + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetEditAsync(long lsjlId) + { + var lsjl = await _lsjlRepo.GetByIdAsync(lsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + var dept = await _deptRepo.GetByIdAsync(lsjl.DeptId); + var zk = await _zkRepo.GetByIdAsync(lsjl.GyzkId); + + var records = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == lsjlId); + List chetais; + if (records.Count > 0) + { + chetais = records.OrderBy(r => r.Chetaihao).Select(ToDto).ToList(); + } + else + { + // 首次称重:按车台范围初始化空行 + chetais = new List(); + for (var i = lsjl.ChetaiStart; i <= lsjl.ChetaiEnd; i++) + chetais.Add(new CdsChetaiDto { Chetaihao = i }); + } + + return new CdsEditDto + { + LsjlId = lsjl.Id, + DeptName = dept?.Name ?? "", + Jizu = lsjl.Jizu, + Huishu = lsjl.Huishu, + ZhuangKouName = zk?.ZhuangKouName ?? "", + Yanse = lsjl.Yanse, + Isbanche = lsjl.Isbanche, + IsbancheName = lsjl.Isbanche ? "半车" : "整车", + Guige = lsjl.Guige, + ChetaiStart = lsjl.ChetaiStart, + ChetaiEnd = lsjl.ChetaiEnd, + Chetaishu = lsjl.Chetaishu, + YingchengNum = YingchengNum(lsjl), + Chetais = chetais + }; + } + + public async Task SaveAsync(CdsSaveDto dto) + { + if (dto.LsjlId == 0) throw new ServiceException("落丝记录无效"); + var lsjl = await _lsjlRepo.GetByIdAsync(dto.LsjlId); + if (lsjl == null) throw new ServiceException("落丝记录不存在", 404); + + if (dto.Chetais == null || dto.Chetais.Count == 0) throw new ServiceException("请先录入丝量数据"); + + var invalid = dto.Chetais.Where(i => i.Chetaihao < lsjl.ChetaiStart || i.Chetaihao > lsjl.ChetaiEnd).ToList(); + if (invalid.Count > 0) throw new ServiceException($"车台号超出范围 [{lsjl.ChetaiStart}-{lsjl.ChetaiEnd}]"); + + // 车台明细:个数 = 丝量 > 0 的个数;小计丝量 = 4 个丝量之和 + var items = dto.Chetais.Select(i => + { + var t = new CdsChetaiDto + { + Chetaihao = i.Chetaihao, + Sl1 = Math.Max(0, i.Sl1), Sl2 = Math.Max(0, i.Sl2), + Sl3 = Math.Max(0, i.Sl3), Sl4 = Math.Max(0, i.Sl4) + }; + CalChetai(t); + return t; + }).OrderBy(i => i.Chetaihao).ToList(); + + var num = items.Sum(x => x.Num); + if (num == 0) throw new ServiceException("请至少录入一个丝量值"); + + // 车台明细先删后建 + var olds = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == dto.LsjlId); + if (olds.Count > 0) await _cdsChetaiRepo.HardDeleteRangeAsync(olds); + + foreach (var t in items) + { + await _cdsChetaiRepo.AddAsync(new CdsChetai + { + LsjlId = dto.LsjlId, + Chetaihao = t.Chetaihao, + Sl1 = t.Sl1, Sl2 = t.Sl2, Sl3 = t.Sl3, Sl4 = t.Sl4, + Num = t.Num, Siliang = t.Siliang + }); + } + + var siliang = items.Sum(x => x.Siliang); + + // 汇总:茧量 / 缫折 / 实绩台产 + var cdsLsjl = await _cdsLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == dto.LsjlId); + if (cdsLsjl == null) + { + cdsLsjl = new CdsLsjl { LsjlId = dto.LsjlId }; + await _cdsLsjlRepo.AddAsync(cdsLsjl); + } + cdsLsjl.Num = num; + cdsLsjl.Siliang = siliang; + CalZhibiao(cdsLsjl, lsjl); + await _cdsLsjlRepo.UpdateAsync(cdsLsjl); + } + + /// 清除称重数据(汇总 + 车台明细物理删除,便于重新称重) + public async Task DeleteAsync(long lsjlId) + { + if (lsjlId == 0) throw new ServiceException("落丝记录无效"); + var chetais = await _cdsChetaiRepo.FindAsync(x => x.LsjlId == lsjlId); + if (chetais.Count > 0) await _cdsChetaiRepo.HardDeleteRangeAsync(chetais); + + var lsjlSum = await _cdsLsjlRepo.FirstOrDefaultAsync(x => x.LsjlId == lsjlId); + if (lsjlSum != null) await _cdsLsjlRepo.HardDeleteAsync(lsjlSum); + } + + // ==================== 计算口径(对齐 F9 Cds_Lsjl_CalZhibiaoCommand) ==================== + + /// 应秤数 = 车台数 × 4 ×(半车 0.5 / 整车 1) + private static int YingchengNum(LsjlRecord lsjl) + => (int)Math.Round(lsjl.Chetaishu * 4 * (lsjl.Isbanche ? 0.5 : 1), MidpointRounding.AwayFromZero); + + private static void CalChetai(CdsChetaiDto item) + { + item.Num = (item.Sl1 > 0 ? 1 : 0) + (item.Sl2 > 0 ? 1 : 0) + (item.Sl3 > 0 ? 1 : 0) + (item.Sl4 > 0 ? 1 : 0); + item.Siliang = item.Sl1 + item.Sl2 + item.Sl3 + item.Sl4; + } + + /// + /// 茧量 = 落丝桶数 × 每桶茧量;缫折 = 茧量 / 丝量 × 100;实绩台产 = 丝量 / 落丝时长 / 车台数。 + /// F9 口径:缫折 ≤ 100 视为异常数据,不落指标。 + /// + private static void CalZhibiao(CdsLsjl target, LsjlRecord lsjl) + { + if (lsjl.Meitongjianliang == 0 || lsjl.Tongshu == 0) + { + target.Jianliang = 0; target.Saozhe = 0; target.Taichan = 0; + return; + } + if (target.Num <= 0 || target.Siliang <= 0 || lsjl.Hours <= 0 || lsjl.Chetaishu <= 0) + { + target.Jianliang = 0; target.Saozhe = 0; target.Taichan = 0; + return; + } + + var jianliang = lsjl.Tongshu * lsjl.Meitongjianliang; + var saozhe = Math.Round((decimal)jianliang / target.Siliang * 100, 2, MidpointRounding.AwayFromZero); + var taichan = Math.Round((decimal)target.Siliang / (decimal)lsjl.Hours / lsjl.Chetaishu, 2, MidpointRounding.AwayFromZero); + + if (saozhe > 100) + { + target.Jianliang = jianliang; + target.Saozhe = saozhe; + target.Taichan = taichan; + } + else + { + target.Jianliang = jianliang; + target.Saozhe = 0; + target.Taichan = 0; + } + } + + private static CdsChetaiDto ToDto(CdsChetai r) => new() + { + Chetaihao = r.Chetaihao, + Sl1 = r.Sl1, Sl2 = r.Sl2, Sl3 = r.Sl3, Sl4 = r.Sl4, + Num = r.Num, Siliang = r.Siliang + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/GlxdService.cs b/backend/F9WebAiTest.Application/Services/Prod/GlxdService.cs new file mode 100644 index 0000000..c250c3b --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/GlxdService.cs @@ -0,0 +1,109 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 公量纤度:以落丝记录为主表,取纤度检验的平均纤度,由质检录入公量纤度(按日期 + 车间整日批量保存) +public interface IGlxdService +{ + Task> GetListAsync(DateTime? date, long? deptId); + Task SaveAsync(GlxdSaveDto dto); +} + +public class GlxdService : IGlxdService +{ + private readonly IRepository _lsjlRepo; + private readonly IRepository _glxdRepo; + private readonly IRepository _xdjyLsjlRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + + public GlxdService( + IRepository lsjlRepo, + IRepository glxdRepo, + IRepository xdjyLsjlRepo, + IRepository deptRepo, + IRepository zkRepo) + { + _lsjlRepo = lsjlRepo; + _glxdRepo = glxdRepo; + _xdjyLsjlRepo = xdjyLsjlRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + } + + public async Task> GetListAsync(DateTime? date, long? deptId) + { + var lsjls = await _lsjlRepo.GetAllAsync(); + var q = lsjls.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.WorkDate.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var items = q.OrderBy(x => x.DeptId).ThenBy(x => x.Jizu).ThenBy(x => x.Huishu).ToList(); + if (items.Count == 0) return new List(); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + var glxds = (await _glxdRepo.GetAllAsync()).ToDictionary(x => x.LsjlId); + var xdjySums = (await _xdjyLsjlRepo.GetAllAsync()).ToDictionary(x => x.LsjlId); + + return items.Select(x => + { + glxds.TryGetValue(x.Id, out var g); + xdjySums.TryGetValue(x.Id, out var xs); + return new GlxdListItemDto + { + LsjlId = x.Id, + WorkDate = x.WorkDate, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + Jizu = x.Jizu, + Huishu = x.Huishu, + Guige = x.Guige, + ZhuangKouName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Yanse = x.Yanse, + Isbanche = x.Isbanche, + IsbancheName = x.Isbanche ? "半车" : "整车", + Chetaishu = x.Chetaishu, + Avgxiandu = xs?.Avgxiandu ?? 0, + Gongliangxiandu = g?.Gongliangxiandu ?? 0 + }; + }).ToList(); + } + + public async Task SaveAsync(GlxdSaveDto dto) + { + if (dto.Items == null || dto.Items.Count == 0) throw new ServiceException("没有需要保存的公量纤度"); + + // 校验落丝记录存在,并按落丝记录 Id 去重 + var dict = new Dictionary(); + foreach (var it in dto.Items) + { + if (it.LsjlId == 0) throw new ServiceException("存在无效的落丝记录"); + var lsjl = await _lsjlRepo.GetByIdAsync(it.LsjlId); + if (lsjl == null) throw new ServiceException($"落丝记录 {it.LsjlId} 不存在", 404); + dict[it.LsjlId] = it.Gongliangxiandu; + } + + foreach (var (lsjlId, value) in dict) + { + var e = await _glxdRepo.FirstOrDefaultAsync(x => x.LsjlId == lsjlId); + if (e == null) + { + e = new Glxd { LsjlId = lsjlId, Gongliangxiandu = value }; + await _glxdRepo.AddAsync(e); + } + else + { + e.Gongliangxiandu = value; + await _glxdRepo.UpdateAsync(e); + } + } + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs b/backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs new file mode 100644 index 0000000..2cbb01a --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/KaoqinService.cs @@ -0,0 +1,890 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.PayRoll; +using F9WebAiTest.Domain.Entities.Prod; +using F9WebAiTest.Domain.Enums.SYS.Departments; +using F9WebAiTest.Domain.Enums.SYS.Employees; +using SysZq = F9WebAiTest.Domain.Entities.SYS.Zq; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; +using SysEmp = F9WebAiTest.Domain.Entities.SYS.Employees; +using SysWt = F9WebAiTest.Domain.Entities.SYS.WorkTypes; + +namespace F9WebAiTest.Application.Services.Prod; + +/// +/// 职工考勤-考勤登记(CQDJ)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ。 +/// 保存语义与老系统一致:按(日期 + 部门 + 工种 + 班次)先软删旧登记与明细,再整批新增(不做增量 diff)。 +/// +public interface IKaoqinService +{ + /// 取某「日期+部门+工种+班次」的整组登记(未登记的职工按默认 9 小时生成) + Task GetRegisterAsync(DateTime date, long depId, long worktypeId, int banci, string keyword); + + /// 整组保存(覆盖) + Task SaveRegisterAsync(CqdjSaveDto dto); + + /// 删除整组登记(软删) + Task DeleteRegisterAsync(DateTime date, long depId, long worktypeId, int banci); + + /// 「添加职工」可选职工(本部门在职、未登记过的) + Task> GetEmployeesAsync(long depId, long worktypeId, int banci); + + // ==================== 考勤统计(CQTJ) ==================== + + /// 统计分页(库中的月快照) + Task> GetCqtjPagedAsync(int year, int month, long depId, long worktypeId, + string like, int pageIndex, int pageSize); + + /// 录入:库中已有快照则取库,否则按账期实时汇总 + Task GetCqtjForEditAsync(int year, int month, long depId); + + /// 保存(先软删该 年/月/部门 旧快照,再整批新增) + Task SaveCqtjAsync(CqtjSaveDto dto); + + /// 删除该 年/月/部门 的统计快照 + Task DeleteCqtjAsync(int year, int month, long depId); + + /// 一键生成:遍历全部车间,缺的才生成 + Task GenerateCqtjAllAsync(int year, int month); + + /// 全部删除:删除该年月的全部统计快照 + Task DeleteCqtjAllAsync(int year, int month); +} + +public class KaoqinService : IKaoqinService +{ + // 出勤类型(Cqdj_CqType_Enum,老系统硬编码) + private const int CqZhengchang = 1; + private const int CqQingjia = 2; + private const int CqLiudong = 3; + private const int CqJiaban = 4; + private const int CqDaiban = 5; + private const int CqWaipai = 6; + private const int CqKuanggong = 7; + private const int CqGongshang = 8; + + private const decimal DefaultHours = 9m; // 新职工默认标准 / 出勤时长 + + private readonly IRepository _cqdjRepo; + private readonly IRepository _itemRepo; + private readonly IRepository _cqtjRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _empRepo; + private readonly IRepository _wtRepo; + private readonly IRepository _confGzRepo; + private readonly IRepository _confZqRepo; + private readonly IRepository _confEmpRepo; + private readonly IRepository _gwsrkhRepo; + private readonly IRepository _zhangqiRepo; + + public KaoqinService(IRepository cqdjRepo, IRepository itemRepo, IRepository cqtjRepo, + IRepository deptRepo, IRepository empRepo, + IRepository wtRepo, IRepository confGzRepo, IRepository confZqRepo, + IRepository confEmpRepo, IRepository gwsrkhRepo, IRepository zhangqiRepo) + { + _cqdjRepo = cqdjRepo; + _itemRepo = itemRepo; + _cqtjRepo = cqtjRepo; + _deptRepo = deptRepo; + _empRepo = empRepo; + _wtRepo = wtRepo; + _confGzRepo = confGzRepo; + _confZqRepo = confZqRepo; + _confEmpRepo = confEmpRepo; + _gwsrkhRepo = gwsrkhRepo; + _zhangqiRepo = zhangqiRepo; + } + + // ==================== 查询 ==================== + + public async Task GetRegisterAsync(DateTime date, long depId, long worktypeId, int banci, string keyword) + { + if (depId <= 0) throw new ServiceException("请先选择部门"); + + var day = date.Date; + var nextDay = day.AddDays(1); + var depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name); + var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName); + var emps = await LoadEmployeesAsync(depId, worktypeId, banci); + + // 已保存的登记 + var saved = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay + && x.DepId == depId && x.Banci == banci); + if (worktypeId > 0) saved = saved.Where(x => x.WorktypeId == worktypeId).ToList(); + var savedByEmp = saved.GroupBy(x => x.EmployeeId).ToDictionary(g => g.Key, g => g.First()); + + var ids = saved.Select(x => x.Id).ToList(); + var items = ids.Count == 0 + ? new List() + : await _itemRepo.FindAsync(x => ids.Contains(x.CqdjId)); + + var view = new CqdjViewDto { Date = day, DepId = depId, WorktypeId = worktypeId, Banci = banci }; + + foreach (var emp in emps) + { + if (!string.IsNullOrWhiteSpace(keyword)) + { + var key = keyword.Trim(); + if (!((emp.Name ?? "").Contains(key) || (emp.Code ?? "").Contains(key))) continue; + } + + var row = new CqdjRowDto + { + EmployeeId = emp.Id, + EmployeeCode = emp.Code ?? "", + EmployeeName = emp.Name ?? "", + DepId = emp.DepId, + DepName = depts.TryGetValue(emp.DepId, out var dn) ? dn : "", + WorktypeId = emp.WorktypeId, + WorktypeName = wts.TryGetValue(emp.WorktypeId, out var wn) ? wn : "", + Banci = banci, + IsXuegong = !emp.IsZhuanzheng, + }; + + if (savedByEmp.TryGetValue(emp.Id, out var rec)) + { + row.Id = rec.Id; + row.Saved = true; + row.IsXuegong = rec.IsXuegong; + row.HoursStandard = rec.HoursStandard; + row.HoursCq = rec.HoursCq; + row.HoursQj = rec.HoursQj; + row.HoursLd = rec.HoursLd; + row.HoursJb = rec.HoursJb; + row.HoursDb = rec.HoursDb; + row.HoursWp = rec.HoursWp; + row.HoursKg = rec.HoursKg; + row.HoursGs = rec.HoursGs; + row.CsYeban = rec.CsYeban; + row.Memo = rec.Memo ?? ""; + row.Items = items.Where(i => i.CqdjId == rec.Id) + .OrderBy(i => i.Id) + .Select(ToItemDto).ToList(); + } + else + { + // 未登记:默认标准 9 小时 + 一条「正常」明细 + row.HoursStandard = DefaultHours; + row.HoursCq = DefaultHours; + row.Items.Add(new CqdjItemDto + { + DepId = emp.DepId, + WorktypeId = emp.WorktypeId, + TypeCq = CqZhengchang, + TypeQj = 0, + Hours = DefaultHours, + }); + } + + view.Rows.Add(row); + } + + return view; + } + + // ==================== 保存 ==================== + + public async Task SaveRegisterAsync(CqdjSaveDto dto) + { + if (dto == null) throw new ServiceException("保存内容为空"); + if (dto.Date == default) throw new ServiceException("请选择考勤日期"); + if (dto.DepId <= 0) throw new ServiceException("请选择部门"); + if (dto.Rows == null || dto.Rows.Count == 0) throw new ServiceException("没有需要保存的职工"); + + var day = dto.Date.Date; + var nextDay = day.AddDays(1); + var empDict = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id); + + // 校验 + foreach (var row in dto.Rows) + { + var name = empDict.TryGetValue(row.EmployeeId, out var e) ? e.Name : $"#{row.EmployeeId}"; + var items = row.Items ?? new List(); + if (items.Count == 0) throw new ServiceException($"职工「{name}」没有考勤明细"); + if (items.Count(x => x.TypeCq == CqZhengchang) != 1) + throw new ServiceException($"职工「{name}」必须且只能有一条「正常」明细"); + foreach (var it in items) + { + if (it.TypeCq == CqQingjia && it.TypeQj == 0) throw new ServiceException($"职工「{name}」的请假明细未选择请假类型"); + if (it.TypeCq != CqQingjia && it.TypeQj != 0) throw new ServiceException($"职工「{name}」的非请假明细不能带请假类型"); + if (it.Hours < 0) throw new ServiceException($"职工「{name}」存在负时长"); + } + if (CalcHoursCq(items) < 0) throw new ServiceException($"职工「{name}」的出勤时长不能为负,请检查请假 / 旷工时长"); + } + + // 整组覆盖:先软删该(日期 + 部门 + 工种 + 班次)的旧登记与明细 + var old = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay + && x.DepId == dto.DepId && x.Banci == dto.Banci); + if (dto.WorktypeId > 0) old = old.Where(x => x.WorktypeId == dto.WorktypeId).ToList(); + if (old.Count > 0) + { + var oldIds = old.Select(x => x.Id).ToList(); + var oldItems = await _itemRepo.FindAsync(x => oldIds.Contains(x.CqdjId)); + if (oldItems.Count > 0) await _itemRepo.DeleteRangeAsync(oldItems); + await _cqdjRepo.DeleteRangeAsync(old); + } + + var count = 0; + foreach (var row in dto.Rows) + { + var emp = empDict.TryGetValue(row.EmployeeId, out var e) ? e : null; + var items = row.Items ?? new List(); + + var master = new Cqdj + { + Date = day, + DepId = dto.DepId, + WorktypeId = row.WorktypeId > 0 ? row.WorktypeId : (emp?.WorktypeId ?? 0), + EmployeeId = row.EmployeeId, + Banci = dto.Banci, + IsXuegong = row.IsXuegong, + HoursStandard = row.HoursStandard, + HoursCq = CalcHoursCq(items), + HoursQj = Sum(items, CqQingjia), + HoursLd = Sum(items, CqLiudong), + HoursJb = Sum(items, CqJiaban), + HoursDb = Sum(items, CqDaiban), + HoursWp = Sum(items, CqWaipai), + HoursKg = Sum(items, CqKuanggong), + HoursGs = Sum(items, CqGongshang), + CsYeban = row.CsYeban, + Memo = row.Memo ?? "", + }; + await _cqdjRepo.AddAsync(master); + + var entities = items.Select(it => new CqdjItem + { + CqdjId = master.Id, + Date = day, + DepId = it.DepId > 0 ? it.DepId : (row.DepId > 0 ? row.DepId : dto.DepId), + WorktypeId = it.WorktypeId > 0 ? it.WorktypeId : master.WorktypeId, + EmployeeId = row.EmployeeId, + Banci = dto.Banci, + TypeCq = it.TypeCq, + TypeQj = it.TypeQj, + Hours = it.Hours, + Jizu = it.Jizu ?? "", + CtStart = it.CtStart ?? "", + CtEnd = it.CtEnd ?? "", + DangcheNum = it.DangcheNum, + Memo = it.Memo ?? "", + }).ToList(); + await _itemRepo.AddRangeAsync(entities); + count++; + } + + return count; + } + + // ==================== 删除 ==================== + + public async Task DeleteRegisterAsync(DateTime date, long depId, long worktypeId, int banci) + { + if (depId <= 0) throw new ServiceException("请选择部门"); + var day = date.Date; + var nextDay = day.AddDays(1); + var old = await _cqdjRepo.FindAsync(x => x.Date >= day && x.Date < nextDay + && x.DepId == depId && x.Banci == banci); + if (worktypeId > 0) old = old.Where(x => x.WorktypeId == worktypeId).ToList(); + if (old.Count == 0) return 0; + var oldIds = old.Select(x => x.Id).ToList(); + var oldItems = await _itemRepo.FindAsync(x => oldIds.Contains(x.CqdjId)); + if (oldItems.Count > 0) await _itemRepo.DeleteRangeAsync(oldItems); + await _cqdjRepo.DeleteRangeAsync(old); + return old.Count; + } + + // ==================== 下拉 ==================== + + public async Task> GetEmployeesAsync(long depId, long worktypeId, int banci) + { + if (depId <= 0) return new List(); + var depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name); + var wts = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName); + var emps = await LoadEmployeesAsync(depId, worktypeId, banci); + return emps.Select(e => new CqdjEmpOptionDto + { + Id = e.Id, + Code = e.Code ?? "", + Name = e.Name ?? "", + DepId = e.DepId, + DepName = depts.TryGetValue(e.DepId, out var dn) ? dn : "", + WorktypeId = e.WorktypeId, + WorktypeName = wts.TryGetValue(e.WorktypeId, out var wn) ? wn : "", + IsXuegong = !e.IsZhuanzheng, + }).ToList(); + } + + // ==================== 考勤统计(CQTJ) ==================== + + public async Task> GetCqtjPagedAsync(int year, int month, long depId, long worktypeId, + string like, int pageIndex, int pageSize) + { + var dicts = await LoadNameDictsAsync(); + var all = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month); + if (depId > 0) all = all.Where(x => x.DepId == depId); + if (worktypeId > 0) all = all.Where(x => x.WorktypeId == worktypeId); + + var rows = all.Select(x => ToCqtjDto(x, dicts)).ToList(); + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + rows = rows.Where(x => (x.EmployeeName ?? "").Contains(key) || (x.EmployeeCode ?? "").Contains(key)).ToList(); + } + + var total = rows.Count; + var items = rows + .OrderBy(x => x.DepId).ThenBy(x => x.WorktypeId).ThenBy(x => x.EmployeeCode) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + return new PageResult { Items = items, Total = total, PageIndex = pageIndex, PageSize = pageSize }; + } + + public async Task GetCqtjForEditAsync(int year, int month, long depId) + { + if (depId <= 0) throw new ServiceException("请选择部门"); + var dicts = await LoadNameDictsAsync(); + var exist = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month && x.DepId == depId) + .OrderBy(x => x.WorktypeId).ThenBy(x => x.EmployeeId).ToList(); + + var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + var view = new CqtjViewDto + { + Year = year, Month = month, DepId = depId, + DepName = dicts.Depts.TryGetValue(depId, out var dn) ? dn : "", + QsQuanqinDays = confZq?.QsQuanqinDays ?? 0, + Exists = exist.Count > 0 + }; + view.Rows = exist.Count > 0 + ? exist.Select(x => ToCqtjDto(x, dicts)).ToList() + : await CalcCqtjAsync(year, month, depId); + return view; + } + + public async Task SaveCqtjAsync(CqtjSaveDto dto) + { + if (dto == null) throw new ServiceException("保存内容为空"); + if (dto.DepId <= 0) throw new ServiceException("请选择部门"); + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == dto.Year && x.Month == dto.Month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!"); + + var old = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == dto.Year && x.Month == dto.Month && x.DepId == dto.DepId).ToList(); + if (old.Count > 0) await _cqtjRepo.DeleteRangeAsync(old); + + var list = (dto.Rows ?? new List()).Select(x => new Cqtj + { + Year = dto.Year, Month = dto.Month, + DepId = dto.DepId, WorktypeId = x.WorktypeId, EmployeeId = x.EmployeeId, + DaysCqStandard = x.DaysCqStandard, DaysCqActual = x.DaysCqActual, CqTianci = x.CqTianci, + HoursCq = x.HoursCq, HoursQj = x.HoursQj, HoursLd = x.HoursLd, HoursJb = x.HoursJb, + HoursDb = x.HoursDb, HoursWp = x.HoursWp, HoursKg = x.HoursKg, CsYeban = x.CsYeban, + HoursQjShijia = x.HoursQjShijia, HoursQjBj = x.HoursQjBj, HoursQjHj = x.HoursQjHj, + HoursQjCj = x.HoursQjCj, HoursQjLx = x.HoursQjLx, HoursQjSangjia = x.HoursQjSangjia, + HoursQjGj = x.HoursQjGj, HoursQjNj = x.HoursQjNj, HoursQjTx = x.HoursQjTx, + HoursXgCq = x.HoursXgCq, HoursXgJiaban = x.HoursXgJiaban, + HoursFadingjiaqi = x.HoursFadingjiaqi, HoursQjQqmc = x.HoursQjQqmc, HoursQjQqcal = x.HoursQjQqcal, + JineQuanqin = x.JineQuanqin, JineJb = x.JineJb, JineLiudong = x.JineLiudong, + JineDaiban = x.JineDaiban, JineYb = x.JineYb, JineKgKk = x.JineKgKk, + JineJqBzHj = x.JineJqBzHj, JineJqBzBj = x.JineJqBzBj, JineJqBzLx = x.JineJqBzLx, + JineJqBzSangjia = x.JineJqBzSangjia, JineJqBzFading = x.JineJqBzFading, + JineJqBzTxnj = x.JineJqBzTxnj, JineJqBz = x.JineJqBz, + Memo = x.Memo ?? "" + }).ToList(); + await _cqtjRepo.AddRangeAsync(list); + return list.Count; + } + + public async Task DeleteCqtjAsync(int year, int month, long depId) + { + if (depId <= 0) throw new ServiceException("请选择部门"); + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!"); + var old = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month && x.DepId == depId).ToList(); + if (old.Count == 0) return 0; + await _cqtjRepo.DeleteRangeAsync(old); + return old.Count; + } + + public async Task GenerateCqtjAllAsync(int year, int month) + { + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法生成!"); + var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (confZq == null || confZq.QsQuanqinDays <= 0) + throw new ServiceException("请先录入薪资配置账期的前缫出勤天数!"); + + var depts = (await _deptRepo.GetAllAsync()) + .Where(d => d.IsActive && d.ManageType != Department_ManageType_Enum.公司) + .ToList(); + var saved = (await _cqtjRepo.GetAllAsync()) + .Where(x => x.Year == year && x.Month == month) + .Select(x => x.DepId).Distinct().ToHashSet(); + + var count = 0; + foreach (var d in depts) + { + if (saved.Contains(d.Id)) continue; + var rows = await CalcCqtjAsync(year, month, d.Id); + if (rows.Count == 0) continue; + await SaveCqtjAsync(new CqtjSaveDto { Year = year, Month = month, DepId = d.Id, Rows = rows }); + count++; + } + return count; + } + + public async Task DeleteCqtjAllAsync(int year, int month) + { + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null && zq.IsZhaZhang) throw new ServiceException("账期已扎帐,无法修改!"); + var old = (await _cqtjRepo.GetAllAsync()).Where(x => x.Year == year && x.Month == month).ToList(); + if (old.Count == 0) return 0; + await _cqtjRepo.DeleteRangeAsync(old); + return old.Count; + } + + // ==================== 私有 ==================== + + private class NameDicts + { + public Dictionary Depts = new(); + public Dictionary Worktypes = new(); + public Dictionary Emps = new(); + } + + private async Task LoadNameDictsAsync() => new() + { + Depts = (await _deptRepo.GetAllAsync()).ToDictionary(d => d.Id, d => d.Name), + Worktypes = (await _wtRepo.GetAllAsync()).ToDictionary(w => w.Id, w => w.WorkName), + Emps = (await _empRepo.GetAllAsync()).ToDictionary(e => e.Id) + }; + + private static CqtjRowDto ToCqtjDto(Cqtj x, NameDicts d) => new() + { + Id = x.Id, Year = x.Year, Month = x.Month, + DepId = x.DepId, DepName = d.Depts.TryGetValue(x.DepId, out var dn) ? dn : "", + WorktypeId = x.WorktypeId, WorktypeName = d.Worktypes.TryGetValue(x.WorktypeId, out var wn) ? wn : "", + EmployeeId = x.EmployeeId, + EmployeeCode = d.Emps.TryGetValue(x.EmployeeId, out var e) ? (e.Code ?? "") : "", + EmployeeName = d.Emps.TryGetValue(x.EmployeeId, out var e2) ? (e2.Name ?? "") : "", + DaysCqStandard = x.DaysCqStandard, DaysCqActual = x.DaysCqActual, CqTianci = x.CqTianci, + HoursCq = x.HoursCq, HoursQj = x.HoursQj, HoursLd = x.HoursLd, HoursJb = x.HoursJb, + HoursDb = x.HoursDb, HoursWp = x.HoursWp, HoursKg = x.HoursKg, CsYeban = x.CsYeban, + HoursQjShijia = x.HoursQjShijia, HoursQjBj = x.HoursQjBj, HoursQjHj = x.HoursQjHj, + HoursQjCj = x.HoursQjCj, HoursQjLx = x.HoursQjLx, HoursQjSangjia = x.HoursQjSangjia, + HoursQjGj = x.HoursQjGj, HoursQjNj = x.HoursQjNj, HoursQjTx = x.HoursQjTx, + HoursXgCq = x.HoursXgCq, HoursXgJiaban = x.HoursXgJiaban, + HoursFadingjiaqi = x.HoursFadingjiaqi, HoursQjQqmc = x.HoursQjQqmc, HoursQjQqcal = x.HoursQjQqcal, + JineQuanqin = x.JineQuanqin, JineJb = x.JineJb, JineLiudong = x.JineLiudong, + JineDaiban = x.JineDaiban, JineYb = x.JineYb, JineKgKk = x.JineKgKk, + JineJqBzHj = x.JineJqBzHj, JineJqBzBj = x.JineJqBzBj, JineJqBzLx = x.JineJqBzLx, + JineJqBzSangjia = x.JineJqBzSangjia, JineJqBzFading = x.JineJqBzFading, + JineJqBzTxnj = x.JineJqBzTxnj, JineJqBz = x.JineJqBz, + Memo = x.Memo ?? "" + }; + + /// 账期(取不到时按自然月兜底) + private async Task<(DateTime Start, DateTime End)> GetZhangqiRangeAsync(int year, int month) + { + var zq = await _zhangqiRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + if (zq != null) return (zq.StartDate.Date, zq.EndDate.Date.AddDays(1).AddSeconds(-1)); + var start = new DateTime(year, month, 1); + return (start, start.AddMonths(1).AddSeconds(-1)); + } + + /// + /// 按账期汇总某部门的考勤统计(对齐 Page_Cqtj.CqtjViews)。 + /// 全勤奖涉及「该职工在全部部门 / 工种」的合计出勤 / 请假 / 旷工,故登记数据按账期全量加载。 + /// + private async Task> CalcCqtjAsync(int year, int month, long depId) + { + var (start, end) = await GetZhangqiRangeAsync(year, month); + var confZq = await _confZqRepo.FirstOrDefaultAsync(x => x.Year == year && x.Month == month); + var qsDays = confZq?.QsQuanqinDays ?? 0; + if (qsDays <= 0) throw new ServiceException("请先在【薪资配置账期】功能中设置当月前缫出勤天数!"); + + var dicts = await LoadNameDictsAsync(); + var cqdjAll = await _cqdjRepo.FindAsync(x => x.Date >= start && x.Date <= end); + if (cqdjAll.Count == 0) return new List(); + var ids = cqdjAll.Select(x => x.Id).ToList(); + var itemsAll = await _itemRepo.FindAsync(x => ids.Contains(x.CqdjId)); + + var confGzAll = await _confGzRepo.GetAllAsync(); + var confEmpAll = await _confEmpRepo.GetAllAsync(); + var gwsrkhAll = (await _gwsrkhRepo.GetAllAsync()).Where(x => x.Year == year && x.Month == month).ToList(); + var deptsAll = await _deptRepo.GetAllAsync(); + + // 每职工全部门合计(全勤奖口径) + var cqHoursByEmp = cqdjAll.GroupBy(x => x.EmployeeId).Select(g => new + { + EmployeeId = g.Key, + CqHours = g.Sum(x => x.HoursCq), + JiabanHours = g.Sum(x => x.HoursJb), + QjHours = g.Sum(x => x.HoursQj), + KgHours = g.Sum(x => x.HoursKg) + }).ToDictionary(x => x.EmployeeId); + + var groups = cqdjAll.Where(x => x.DepId == depId) + .GroupBy(x => new { x.EmployeeId, x.DepId, x.WorktypeId }) + .Select(g => new + { + g.Key.EmployeeId, g.Key.DepId, g.Key.WorktypeId, + HoursCq = g.Sum(x => x.HoursCq), + HoursQj = g.Sum(x => x.HoursQj), + HoursLd = g.Sum(x => x.HoursLd), + HoursJb = g.Sum(x => x.HoursJb), + HoursDb = g.Sum(x => x.HoursDb), + HoursWp = g.Sum(x => x.HoursWp), + HoursKg = g.Sum(x => x.HoursKg), + CsYeban = g.Sum(x => x.CsYeban), + List = g.ToList() + }).ToList(); + + var result = new List(); + foreach (var item in groups) + { + if (item.HoursCq + item.HoursJb + item.HoursQj + item.HoursLd + item.HoursDb + + item.HoursKg + item.CsYeban == 0) continue; + + var empDepWtIds = item.List.Select(x => x.Id).ToHashSet(); + var depItems = itemsAll.Where(x => empDepWtIds.Contains(x.CqdjId)).ToList(); + var confEmp = confEmpAll.FirstOrDefault(x => x.EmployeeId == item.EmployeeId && x.WorktypeId == item.WorktypeId); + var confGz = confGzAll.FirstOrDefault(x => x.WorkTypeId == item.WorktypeId); + dicts.Emps.TryGetValue(item.EmployeeId, out var emp); + + var row = new CqtjRowDto + { + Year = year, Month = month, + DepId = item.DepId, DepName = dicts.Depts.TryGetValue(item.DepId, out var dn) ? dn : "", + WorktypeId = item.WorktypeId, WorktypeName = dicts.Worktypes.TryGetValue(item.WorktypeId, out var wn) ? wn : "", + EmployeeId = item.EmployeeId, + EmployeeCode = emp?.Code ?? "", EmployeeName = emp?.Name ?? "", + HoursCq = item.HoursCq, HoursQj = item.HoursQj, HoursLd = item.HoursLd, + HoursJb = item.HoursJb, HoursDb = item.HoursDb, HoursWp = item.HoursWp, + HoursKg = item.HoursKg, CsYeban = item.CsYeban + }; + + if (confGz == null) { result.Add(row); continue; } + + // 学工时长 + var xgList = item.List.Where(x => x.IsXuegong).ToList(); + var xgIds = xgList.Select(x => x.Id).ToHashSet(); + var xgItems = itemsAll.Where(x => xgIds.Contains(x.CqdjId)).ToList(); + row.HoursXgCq = xgList.Sum(x => x.HoursCq); + row.HoursXgJiaban = xgList.Sum(x => x.HoursJb); + + // 法定假 + var fadingBzHours = confEmp?.HoursFadingBz ?? 0; + var fadingGzHours = confEmp?.HoursFadingGz ?? 0; + var jineFadingBz = jisuanFadingBz(fadingBzHours, fadingGzHours, confGz); + row.HoursFadingjiaqi = R2(fadingBzHours + fadingGzHours); + + // 假期补助 + var bz = jiaqiBuzhuJisuan(confGzAll, depItems, jineFadingBz); + row.HoursQjShijia = bz.HoursShijia; row.HoursQjBj = bz.HoursBj; row.HoursQjHj = bz.HoursHj; + row.HoursQjCj = bz.HoursCj; row.HoursQjLx = bz.HoursLx; row.HoursQjSangjia = bz.HoursSangjia; + row.HoursQjGj = bz.HoursGj; row.HoursQjNj = bz.HoursNj; row.HoursQjTx = bz.HoursTx; + row.JineJqBzHj = bz.JineHj; row.JineJqBzBj = bz.JineBj; row.JineJqBzLx = bz.JineLx; + row.JineJqBzSangjia = bz.JineSangjia; row.JineJqBzFading = jineFadingBz; + row.JineJqBzTxnj = bz.JineTxnj; row.JineJqBz = bz.JineTotal; + + var daixinQjHours = bz.HoursNj + bz.HoursTx + bz.HoursGj + bz.HoursLx + bz.HoursHj + bz.HoursSangjia + bz.HoursCj; + var empSum = cqHoursByEmp.TryGetValue(item.EmployeeId, out var s) ? s : null; + var sumCqHours = empSum?.CqHours ?? 0; + var sumJiabanHours = empSum?.JiabanHours ?? 0; + var sumQjHours = empSum?.QjHours ?? 0; + var sumKgHours = empSum?.KgHours ?? 0; + + // 加班免扣请假 / 全勤计算请假 + row.HoursQjQqmc = jisuanJiabanMkHours(confGz, sumJiabanHours, item.HoursQj, qsDays, item.HoursCq); + var sumDaixinHours = sumDaixinHoursJisuan(itemsAll, item.EmployeeId); + var qqJisuanQjHours = sumQjHours - row.HoursQjQqmc - sumDaixinHours; + var depQj = item.HoursQj - row.HoursQjQqmc - daixinQjHours; + row.HoursQjQqcal = depQj < 0 ? 0 : depQj; + + // 全勤奖 + var hoursPerday = confGz.HoursPerday; + if (sumCqHours <= 15 * hoursPerday && emp != null && emp.WorkStatus == Employee_WorkStatus_Enum.离职) + row.JineQuanqin = 0; + else if (confGz.JineQuanqin > 0) + row.JineQuanqin = quanqin(confGz, sumCqHours, sumQjHours, item.HoursCq, item.HoursJb, + row.HoursQjQqcal, qqJisuanQjHours, daixinQjHours, confEmp, sumDaixinHours, sumCqHours, + sumKgHours, emp, start, end, qsDays, + deptsAll.FirstOrDefault(d => d.Id == item.DepId), sumJiabanHours); + + // 加班费 / 流动 / 代班 / 夜班 / 旷工 + var gwsrkh = gwsrkhAll.FirstOrDefault(x => x.EmployeeId == item.EmployeeId); + row.JineJb = jisuanJianbanJine(depItems, confGzAll, xgItems, gwsrkh?.KhJieguo); + row.JineLiudong = liudongJineJisuan(depItems, confGz); + row.JineDaiban = daibanJineJisuan(depItems, confGzAll); + row.JineYb = R2(confGz.JineYeban * item.CsYeban); + row.JineKgKk = R2(confGz.JiaqiJineKuangongKk * item.HoursKg); + + row.CqTianci = jisuanCqTianci(item.List); + row.DaysCqStandard = confGz.QqDaysStandard; + row.DaysCqActual = hoursPerday == 0 ? 0 : R2(item.HoursCq / hoursPerday); + + result.Add(row); + } + return result; + } + + // ---------- 口径函数(对齐 Page_Cqtj 的同名方法) ---------- + + private static decimal R2(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero); + + private class JiaqiBuzhu + { + public decimal HoursShijia, HoursBj, HoursHj, HoursCj, HoursLx, HoursSangjia, HoursGj, HoursNj, HoursTx; + public decimal JineHj, JineBj, JineLx, JineSangjia, JineTxnj, JineTotal; + } + + /// 假期补助 + 请假分项时长(按明细的 请假类型 × 出勤工种 分组计算) + private static JiaqiBuzhu jiaqiBuzhuJisuan(List confGzAll, List items, decimal jineFadingBz) + { + var r = new JiaqiBuzhu(); + var groups = items.GroupBy(x => new { x.TypeQj, x.WorktypeId }) + .Select(g => new { Hours = g.Sum(x => x.Hours), g.Key.TypeQj, g.Key.WorktypeId }).ToList(); + foreach (var g in groups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + switch (g.TypeQj) + { + case 2: // 病假 + r.HoursBj = g.Hours; + r.JineBj += R2((cfg?.JiaqiJineBingjiaBz ?? 0) * g.Hours); break; + case 10: // 轮休 + r.HoursLx = g.Hours; + r.JineLx += R2((cfg?.JiaqiJineLunxiuBz ?? 0) * g.Hours); break; + case 4: // 婚假 + r.HoursHj = g.Hours; + r.JineHj += R2((cfg?.JiaqiJineHunjiaBz ?? 0) * g.Hours); break; + case 5: // 丧假 + r.HoursSangjia = g.Hours; + r.JineSangjia += R2((cfg?.JiaqiJineSangjiaBz ?? 0) * g.Hours); break; + case 3: // 产假 + r.HoursCj = g.Hours; break; + case 1: // 事假 + r.HoursShijia = g.Hours; break; + case 7: // 年假 + r.HoursNj = g.Hours; + r.JineTxnj += R2((cfg?.Jiaqi_Jine_NianjiaBz ?? 0) * g.Hours); break; + case 6: // 公假 + r.HoursGj = g.Hours; break; + case 9: // 调休 + r.HoursTx = g.Hours; + r.JineTxnj += R2((cfg?.Jiaqi_Jine_NianjiaBz ?? 0) * g.Hours); break; + } + } + r.JineTotal = r.JineHj + r.JineBj + r.JineLx + r.JineSangjia + jineFadingBz + r.JineTxnj; + return r; + } + + /// 加班免扣请假时长:满勤且加班达到门槛时免扣固定时长 + private static decimal jisuanJiabanMkHours(ConfGz cfg, decimal sumJiabanHours, decimal hoursQj, + decimal qsDays, decimal depCqHours) + { + var manqinHours = qsDays * cfg.HoursPerday; + if (depCqHours + sumJiabanHours >= manqinHours && sumJiabanHours >= cfg.QqHoursDkMinjiaban && sumJiabanHours != 0) + return cfg.QqHoursDkQingjia; + return 0; + } + + /// 带薪请假时长合计(跨部门,按该职工全部明细) + private static decimal sumDaixinHoursJisuan(List items, long employeeId) + => items.Where(x => x.EmployeeId == employeeId && x.TypeQj is 10 or 7 or 6 or 9 or 4 or 3 or 5) + .Sum(x => x.Hours); + + /// 加班费(含学工加班:学工岗位薪资折算) + private static decimal jisuanJianbanJine(List items, List confGzAll, + List xgItems, string khJieguo) + { + decimal jine = 0; + var groups = items.Where(x => x.TypeCq == CqJiaban) + .GroupBy(x => x.WorktypeId) + .Select(g => new { Hours = g.Sum(x => x.Hours), WorktypeId = g.Key }).ToList(); + foreach (var g in groups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + decimal price = cfg?.JiabanJineBz ?? 0; + if (!string.IsNullOrWhiteSpace(khJieguo) && cfg != null) + { + price = khJieguo switch + { + "欠合格" => cfg.Jiaban_GwsrkhJineQianhege, + "期望" => cfg.Jiaban_GwsrkhJineQiwang, + "合格" => cfg.Jiaban_GwsrkhJineHege, + "胜任" => cfg.Jiaban_GwsrkhJineShengren, + "超胜任" => cfg.Jiaban_GwsrkhJineChaoshengren, + _ => cfg.JiabanJineBz + }; + } + jine += g.Hours * price; + } + + var xgGroups = xgItems.Where(x => x.TypeCq == CqJiaban) + .GroupBy(x => x.WorktypeId) + .Select(g => new { Hours = g.Sum(x => x.Hours), WorktypeId = g.Key }).ToList(); + foreach (var g in xgGroups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + if (cfg == null) continue; + var std = cfg.QqDaysStandard * cfg.HoursPerday; + if (std == 0) continue; + jine += cfg.XgJineGangwei / std * g.Hours; + } + return R2(jine); + } + + /// 流动工资:工种配置的固定流动工资(元/小时)× 流动时长 + private static decimal liudongJineJisuan(List items, ConfGz cfg) + { + decimal jine = 0; + var groups = items.Where(x => x.TypeCq == CqLiudong) + .GroupBy(x => new { x.DepId, x.WorktypeId }) + .Select(g => new { Hours = g.Sum(x => x.Hours) }).ToList(); + foreach (var g in groups) jine += R2(cfg.LiudongJineGuding * g.Hours); + return R2(jine); + } + + /// 代班工资:代班所在工种岗位薪资 / 标准出勤时长 × 代班时长 + private static decimal daibanJineJisuan(List items, List confGzAll) + { + decimal jine = 0; + var groups = items.Where(x => x.TypeCq == CqDaiban) + .GroupBy(x => new { x.DepId, x.WorktypeId }) + .Select(g => new { Hours = g.Sum(x => x.Hours), g.Key.WorktypeId }).ToList(); + foreach (var g in groups) + { + var cfg = confGzAll.FirstOrDefault(x => x.WorkTypeId == g.WorktypeId); + if (cfg == null) continue; + var std = cfg.QqDaysStandard * cfg.HoursPerday; + if (std == 0) continue; + jine += cfg.JineGangwei / std * g.Hours; + } + return R2(jine); + } + + /// 出勤天次:(出勤 + 加班) > 0 的天数 + private static decimal jisuanCqTianci(List list) + => list.Count(x => x.HoursCq + x.HoursJb > 0); + + /// 法定假补助:补助小时 × 补助单价 + 工资小时 × 工资单价 + private static decimal jisuanFadingBz(decimal hoursFadingBz, decimal hoursFadingGz, ConfGz cfg) + { + if (hoursFadingBz + hoursFadingGz <= 0) return 0; + return R2(hoursFadingBz * cfg.JiaqiJineFadingBz + hoursFadingGz * cfg.JiaqiJineFadingGz); + } + + /// + /// 全勤奖(对齐 Page_Cqtj.quanqin)。 + /// sumCqHours / sumQjHours / sumKgHours 为该职工全部门合计;depCqHours / depHoursQj 为本部门。 + /// + private decimal quanqin(ConfGz cfg, decimal sumCqHours, decimal sumQjHours, decimal depCqHours, decimal hoursJb, + decimal depHoursQj, decimal qqJisuanQjHours, decimal daixinQjHours, ConfEmployee confEmp, + decimal sumDaixinHours, decimal sumCqHours2, decimal sumKgHours, SysEmp.Employee emp, + DateTime zqStart, DateTime zqEnd, decimal qsDays, SysDept.Department dep, decimal sumJiabanHours) + { + var hoursPerday = cfg.HoursPerday; + var jineQuanqin = confEmp != null && confEmp.IsXuegong ? cfg.XgJineQuanqin : cfg.JineQuanqin; + var qqHoursMaxQj = cfg.QqHoursMaxQj; + var standardCqHours = cfg.QqDaysStandard * hoursPerday; + if (hoursPerday <= 0) return 0; + + var ruzhi = emp?.RuzhiDate; + var lizhi = emp?.LizhiDate; + bool InPeriod(DateTime? d) => d.HasValue && d.Value.Date >= zqStart.Date && d.Value.Date <= zqEnd.Date; + + // 1、总旷工超过半天(含半天)不计全勤 + if (sumKgHours >= hoursPerday / 2) return 0; + // 2、出勤总时长小于等于 3 天不计全勤 + if (sumCqHours <= hoursPerday * 3) return 0; + + if (sumCqHours != depCqHours) // 存在多部门 / 多工种出勤 + { + var shijiCqDays = (sumCqHours + sumDaixinHours) / hoursPerday; + if (InPeriod(lizhi)) + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * depCqHours); + + if (InPeriod(ruzhi)) + { + if (shijiCqDays >= qsDays) return sumCqHours == 0 ? 0 : jineQuanqin / sumCqHours * depCqHours; + if (shijiCqDays < 3) return 0; + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * depCqHours); + } + + var chushi = (sumCqHours + sumDaixinHours) / hoursPerday >= 15 + ? (sumCqHours == 0 ? 0 : jineQuanqin / sumCqHours * depCqHours) + : (standardCqHours == 0 ? 0 : jineQuanqin / standardCqHours * depCqHours); + + if (qqJisuanQjHours >= qqHoursMaxQj) return 0; + return qqHoursMaxQj == 0 ? 0 : R2(chushi - (chushi / qqHoursMaxQj) * qqJisuanQjHours); + } + + var shijiDays = (sumCqHours + sumDaixinHours) / hoursPerday; + if (InPeriod(lizhi)) + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * sumCqHours); + + if (InPeriod(ruzhi)) + { + if (shijiDays >= qsDays) return jineQuanqin; + if (shijiDays < 3) return 0; + return standardCqHours == 0 ? 0 : R2(jineQuanqin / standardCqHours * sumCqHours); + } + + var chushi2 = (sumCqHours + daixinQjHours) / hoursPerday >= 15 + ? jineQuanqin + : (standardCqHours == 0 ? 0 : jineQuanqin / standardCqHours * sumCqHours); + + // 设备办全职:出勤含加班达到前缫出勤天数且不低于标准全勤时长时,请假不扣全勤 + if (dep != null && dep.DutyType == Department_DutyType_Enum.设备办) + { + var cqhours = sumCqHours + daixinQjHours + sumJiabanHours; + if (cqhours / hoursPerday >= qsDays && cqhours >= standardCqHours) return R2(chushi2); + } + + if (qqJisuanQjHours >= qqHoursMaxQj) return 0; + return qqHoursMaxQj == 0 ? 0 : R2(chushi2 - (chushi2 / qqHoursMaxQj) * depHoursQj); + } + + /// 本部门在职职工(按工种 / 班次过滤,排除已离职、未入职、工种为 0) + private async Task> LoadEmployeesAsync(long depId, long worktypeId, int banci) + { + var all = await _empRepo.GetAllAsync(); + return all.Where(e => e.DepId == depId + && e.WorktypeId != 0 + && e.WorkStatus != Employee_WorkStatus_Enum.离职 + && (worktypeId <= 0 || e.WorktypeId == worktypeId) + && (banci <= 0 || e.Banci == null || (int)e.Banci == banci)) + .OrderBy(e => e.Code).ThenBy(e => e.Name) + .ToList(); + } + + /// 出勤时长 = 正常 + 流动 + 代班 + 外派 + 工伤(不含加班) + private static decimal CalcHoursCq(List items) + => items.Where(x => x.TypeCq == CqZhengchang || x.TypeCq == CqLiudong || x.TypeCq == CqDaiban + || x.TypeCq == CqWaipai || x.TypeCq == CqGongshang) + .Sum(x => x.Hours); + + private static decimal Sum(List items, int typeCq) + => items.Where(x => x.TypeCq == typeCq).Sum(x => x.Hours); + + private static CqdjItemDto ToItemDto(CqdjItem x) => new() + { + Id = x.Id, + DepId = x.DepId, + WorktypeId = x.WorktypeId, + TypeCq = x.TypeCq, + TypeQj = x.TypeQj, + Hours = x.Hours, + Jizu = x.Jizu ?? "", + CtStart = x.CtStart ?? "", + CtEnd = x.CtEnd ?? "", + DangcheNum = x.DangcheNum, + Memo = x.Memo ?? "", + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs b/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs index c34cf1e..cf83a3d 100644 --- a/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs +++ b/backend/F9WebAiTest.Application/Services/Prod/LsjlService.cs @@ -96,6 +96,7 @@ public class LsjlService : ILsjlService e.LuosiTime = d.LuosiTime; e.Yanse = d.Yanse; e.Huishu = d.Huishu; e.ChetaiStart = d.ChetaiStart; e.ChetaiEnd = d.ChetaiEnd; e.Chetaishu = d.Chetaishu; e.Isbanche = d.Isbanche; e.Hours = d.Hours; e.Guige = d.Guige; e.Chesu = d.Chesu; + e.Tongshu = d.Tongshu; e.Meitongjianliang = d.Meitongjianliang; return e; } @@ -110,7 +111,8 @@ public class LsjlService : ILsjlService LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu, ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu, Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车", - Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu + Hours = e.Hours, Tongshu = e.Tongshu, Meitongjianliang = e.Meitongjianliang, + Guige = e.Guige, Chesu = e.Chesu }; } @@ -123,7 +125,8 @@ public class LsjlService : ILsjlService LuosiTime = e.LuosiTime, Yanse = e.Yanse, Huishu = e.Huishu, ChetaiStart = e.ChetaiStart, ChetaiEnd = e.ChetaiEnd, Chetaishu = e.Chetaishu, Isbanche = e.Isbanche, IsbancheName = e.Isbanche ? "半车" : "整车", - Hours = e.Hours, Guige = e.Guige, Chesu = e.Chesu + Hours = e.Hours, Tongshu = e.Tongshu, Meitongjianliang = e.Meitongjianliang, + Guige = e.Guige, Chesu = e.Chesu }; } } diff --git a/backend/F9WebAiTest.Application/Services/Prod/SpjcService.cs b/backend/F9WebAiTest.Application/Services/Prod/SpjcService.cs new file mode 100644 index 0000000..4e76ef1 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/SpjcService.cs @@ -0,0 +1,188 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 丝片检查:按「日期 + 车间 + 工艺庄口 + 规格」记录前缫/后缫疵点片数 +public interface ISpjcService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string like); + Task GetForEditAsync(long id); + Task SaveAsync(SpjcSaveDto dto); + Task DeleteAsync(long id); +} + +public class SpjcService : ISpjcService +{ + private readonly IRepository _spjcRepo; + private readonly IRepository _deptRepo; + private readonly IRepository _zkRepo; + + public SpjcService( + IRepository spjcRepo, + IRepository deptRepo, + IRepository zkRepo) + { + _spjcRepo = spjcRepo; + _deptRepo = deptRepo; + _zkRepo = zkRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime? date, long? deptId, string like) + { + var all = await _spjcRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (date.HasValue) q = q.Where(x => x.Date.Date == date.Value.Date); + if (deptId.HasValue) q = q.Where(x => x.DeptId == deptId.Value); + + var depts = await _deptRepo.GetAllAsync(); + var deptDict = depts.ToDictionary(d => d.Id, d => d.Name); + var zks = await _zkRepo.GetAllAsync(); + var zkDict = zks.ToDictionary(z => z.Id, z => z.ZhuangKouName); + + if (!string.IsNullOrWhiteSpace(like)) + { + var kwDeptIds = depts.Where(d => d.Name.Contains(like)).Select(d => d.Id).ToHashSet(); + var kwZkIds = zks.Where(z => z.ZhuangKouName.Contains(like)).Select(z => z.Id).ToHashSet(); + q = q.Where(x => kwDeptIds.Contains(x.DeptId) || kwZkIds.Contains(x.GyzkId) || x.Guige.Contains(like)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => ToListDto(x, deptDict, zkDict)).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetForEditAsync(long id) + { + var e = await _spjcRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404); + + var dept = await _deptRepo.GetByIdAsync(e.DeptId); + var zk = await _zkRepo.GetByIdAsync(e.GyzkId); + + return new SpjcSaveDto + { + Id = e.Id, + Date = e.Date, + DeptId = e.DeptId, + DeptName = dept?.Name ?? "", + GyzkId = e.GyzkId, + GyzkName = zk?.ZhuangKouName ?? "", + Guige = e.Guige, + Pianshu = e.Pianshu, + PianshuXuegong = e.PianshuXuegong, + QsJiahua = e.QsJiahua, QsYouwu = e.QsYouwu, QsHeidian = e.QsHeidian, + QsShangsi = e.QsShangsi, QsYetiao = e.QsYetiao, QsXiaosipian = e.QsXiaosipian, QsQita = e.QsQita, + HsYouwu = e.HsYouwu, HsShangsi = e.HsShangsi, HsShuangsi = e.HsShuangsi, HsZhisi = e.HsZhisi, + HsZhantiao = e.HsZhantiao, HsYingjiao = e.HsYingjiao, HsGuojiao = e.HsGuojiao, + HsShouchu = e.HsShouchu, HsWuran = e.HsWuran, HsLiansi = e.HsLiansi, HsWdmt = e.HsWdmt, + HsMaoluan = e.HsMaoluan, HsQita = e.HsQita, + Memo = e.Memo + }; + } + + public async Task SaveAsync(SpjcSaveDto dto) + { + if (dto.DeptId == 0) throw new ServiceException("请选择车间"); + if (dto.GyzkId == 0) throw new ServiceException("请选择工艺庄口"); + if (string.IsNullOrWhiteSpace(dto.Guige)) throw new ServiceException("请选择规格"); + + var date = dto.Date.Date; + var guige = dto.Guige.Trim(); + + Spjc e; + if (dto.Id > 0) + { + e = await _spjcRepo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404); + + var dup = await _spjcRepo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Date.Date == date + && x.DeptId == dto.DeptId && x.GyzkId == dto.GyzkId + && x.Guige == guige); + if (dup != null) throw new ServiceException("已存在相同的丝片检查!"); + } + else + { + var exists = await _spjcRepo.AnyAsync(x => x.Date.Date == date && x.DeptId == dto.DeptId + && x.GyzkId == dto.GyzkId && x.Guige == guige); + if (exists) throw new ServiceException("已存在相同的丝片检查!"); + e = new Spjc(); + await _spjcRepo.AddAsync(e); + } + + e.Date = date; + e.DeptId = dto.DeptId; + e.GyzkId = dto.GyzkId; + e.Guige = guige; + e.Pianshu = dto.Pianshu; + e.PianshuXuegong = dto.PianshuXuegong; + e.QsJiahua = dto.QsJiahua; + e.QsYouwu = dto.QsYouwu; + e.QsHeidian = dto.QsHeidian; + e.QsShangsi = dto.QsShangsi; + e.QsYetiao = dto.QsYetiao; + e.QsXiaosipian = dto.QsXiaosipian; + e.QsQita = dto.QsQita; + e.HsYouwu = dto.HsYouwu; + e.HsShangsi = dto.HsShangsi; + e.HsShuangsi = dto.HsShuangsi; + e.HsZhisi = dto.HsZhisi; + e.HsZhantiao = dto.HsZhantiao; + e.HsYingjiao = dto.HsYingjiao; + e.HsGuojiao = dto.HsGuojiao; + e.HsShouchu = dto.HsShouchu; + e.HsWuran = dto.HsWuran; + e.HsLiansi = dto.HsLiansi; + e.HsWdmt = dto.HsWdmt; + e.HsMaoluan = dto.HsMaoluan; + e.HsQita = dto.HsQita; + e.Memo = dto.Memo ?? ""; + + // 合计由服务端重算(与 F9 的 Qs_heji / Hs_heji 一致) + e.QsHeji = e.QsJiahua + e.QsYouwu + e.QsHeidian + e.QsShangsi + e.QsYetiao + e.QsXiaosipian + e.QsQita; + e.HsHeji = e.HsYouwu + e.HsShangsi + e.HsShuangsi + e.HsZhisi + e.HsZhantiao + e.HsYingjiao + + e.HsGuojiao + e.HsShouchu + e.HsWuran + e.HsLiansi + e.HsWdmt + e.HsMaoluan + e.HsQita; + + await _spjcRepo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _spjcRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片检查不存在或已删除", 404); + // 业务键带唯一索引,物理删除避免「删除后重建」撞索引(与煮茧日报同一策略) + await _spjcRepo.HardDeleteAsync(e); + } + + private static SpjcListItemDto ToListDto(Spjc x, Dictionary deptDict, Dictionary zkDict) => new() + { + Id = x.Id, + Date = x.Date, + DeptId = x.DeptId, + DeptName = deptDict.GetValueOrDefault(x.DeptId, ""), + GyzkId = x.GyzkId, + GyzkName = zkDict.GetValueOrDefault(x.GyzkId, ""), + Guige = x.Guige, + Pianshu = x.Pianshu, + PianshuXuegong = x.PianshuXuegong, + QsJiahua = x.QsJiahua, QsYouwu = x.QsYouwu, QsHeidian = x.QsHeidian, + QsShangsi = x.QsShangsi, QsYetiao = x.QsYetiao, QsXiaosipian = x.QsXiaosipian, + QsQita = x.QsQita, QsHeji = x.QsHeji, + HsYouwu = x.HsYouwu, HsShangsi = x.HsShangsi, HsShuangsi = x.HsShuangsi, HsZhisi = x.HsZhisi, + HsZhantiao = x.HsZhantiao, HsYingjiao = x.HsYingjiao, HsGuojiao = x.HsGuojiao, + HsShouchu = x.HsShouchu, HsWuran = x.HsWuran, HsLiansi = x.HsLiansi, HsWdmt = x.HsWdmt, + HsMaoluan = x.HsMaoluan, HsQita = x.HsQita, HsHeji = x.HsHeji, + Memo = x.Memo + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/SptzService.cs b/backend/F9WebAiTest.Application/Services/Prod/SptzService.cs new file mode 100644 index 0000000..5c3b63f --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/SptzService.cs @@ -0,0 +1,159 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Prod; + +namespace F9WebAiTest.Application.Services.Prod; + +/// 丝片台帐:每日一条,记录浸丝 / 返丝 / 编丝 / 整理片数 +public interface ISptzService +{ + Task> GetPagedAsync(int pageIndex, int pageSize, DateTime dateFrom, DateTime dateTo); + Task GetForEditAsync(long id); + Task GetDefaultsAsync(DateTime date); + Task SaveAsync(SptzSaveDto dto); + Task DeleteAsync(long id); +} + +public class SptzService : ISptzService +{ + private readonly IRepository _sptzRepo; + private readonly IRepository _spjcRepo; + + public SptzService(IRepository sptzRepo, IRepository spjcRepo) + { + _sptzRepo = sptzRepo; + _spjcRepo = spjcRepo; + } + + public async Task> GetPagedAsync(int pageIndex, int pageSize, DateTime dateFrom, DateTime dateTo) + { + var all = await _sptzRepo.GetAllAsync(); + var from = dateFrom.Date; + var to = dateTo.Date; + var q = all.AsQueryable().Where(x => x.Date.Date >= from && x.Date.Date <= to); + + var total = q.LongCount(); + var items = q.OrderBy(x => x.Date).Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(ToListDto).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetForEditAsync(long id) + { + var e = await _sptzRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404); + + return new SptzSaveDto + { + Id = e.Id, + Date = e.Date, + DateLast = e.DateLast, + JsShangcun = e.JsShangcun, + JsXiasi = e.JsXiasi, + JsJinsi = e.JsJinsi, + FsFsps = e.FsFsps, + BsBsps = e.BsBsps, + ZlZhengpin = e.ZlZhengpin, + ZlTichu = e.ZlTichu, + Memo = e.Memo + }; + } + + /// + /// 新增默认值:上一本台账日期 + 其上存(= 上一日今存),并按当日「丝片检查」汇总出整理正品/剔除片数。 + /// + public async Task GetDefaultsAsync(DateTime date) + { + var target = date.Date; + var all = await _sptzRepo.GetAllAsync(); + + var latest = all.Where(x => x.Date.Date < target).OrderByDescending(x => x.Date).FirstOrDefault(); + var latestAny = all.OrderByDescending(x => x.Date).FirstOrDefault(); + + var spjcs = (await _spjcRepo.GetAllAsync()).Where(x => x.Date.Date == target).ToList(); + var tichu = spjcs.Sum(x => x.QsJiahua + x.QsYouwu + x.QsHeidian + x.QsShangsi + + x.QsYetiao + x.QsXiaosipian + x.QsQita); + var zhengpin = spjcs.Sum(x => x.Pianshu + x.PianshuXuegong) - tichu; + + return new SptzDefaultsDto + { + Date = target, + DateLast = latest?.Date, + JsShangcun = latest?.JsJincun ?? 0, + LatestDate = latestAny?.Date, + ZlTichu = tichu, + ZlZhengpin = zhengpin, + HasSpjc = spjcs.Count > 0 + }; + } + + public async Task SaveAsync(SptzSaveDto dto) + { + var date = dto.Date.Date; + + Sptz e; + if (dto.Id > 0) + { + e = await _sptzRepo.GetByIdAsync(dto.Id); + if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404); + + var dup = await _sptzRepo.FirstOrDefaultAsync(x => x.Id != dto.Id && x.Date.Date == date); + if (dup != null) throw new ServiceException($"已存在 {date:yyyy-MM-dd} 的丝片台帐!"); + } + else + { + var exists = await _sptzRepo.AnyAsync(x => x.Date.Date == date); + if (exists) throw new ServiceException($"已存在 {date:yyyy-MM-dd} 的丝片台帐!"); + e = new Sptz(); + await _sptzRepo.AddAsync(e); + } + + e.Date = date; + e.DateLast = dto.DateLast == default ? date.AddDays(-1) : dto.DateLast.Date; + e.JsShangcun = dto.JsShangcun; + e.JsXiasi = dto.JsXiasi; + e.JsJinsi = dto.JsJinsi; + e.FsFsps = dto.FsFsps; + e.BsBsps = dto.BsBsps; + e.ZlZhengpin = dto.ZlZhengpin; + e.ZlTichu = dto.ZlTichu; + e.Memo = dto.Memo ?? ""; + + // 派生值由服务端重算(与 F9 一致):上日遗留/今日新增 = 上存 − 浸丝;今存 = 下丝 + 上日遗留/今日新增 + e.JsSrylJrxz = e.JsShangcun - e.JsJinsi; + e.JsJincun = e.JsXiasi + e.JsSrylJrxz; + + await _sptzRepo.UpdateAsync(e); + return e.Id; + } + + public async Task DeleteAsync(long id) + { + var e = await _sptzRepo.GetByIdAsync(id); + if (e == null) throw new ServiceException("丝片台帐不存在或已删除", 404); + // 日期带唯一索引,物理删除避免「删除后重建」撞索引(与煮茧日报同一策略) + await _sptzRepo.HardDeleteAsync(e); + } + + private static SptzListItemDto ToListDto(Sptz x) => new() + { + Id = x.Id, + Date = x.Date, + DateLast = x.DateLast, + JsShangcun = x.JsShangcun, + JsXiasi = x.JsXiasi, + JsJinsi = x.JsJinsi, + JsSrylJrxz = x.JsSrylJrxz, + JsJincun = x.JsJincun, + FsFsps = x.FsFsps, + BsBsps = x.BsBsps, + ZlZhengpin = x.ZlZhengpin, + ZlTichu = x.ZlTichu, + Memo = x.Memo + }; +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/XjrbService.cs b/backend/F9WebAiTest.Application/Services/Prod/XjrbService.cs new file mode 100644 index 0000000..d3ba427 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/XjrbService.cs @@ -0,0 +1,496 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; + +namespace F9WebAiTest.Application.Services.Prod; + +/// +/// 选剥茧(对应 F9 生产-选剥茧 XBJ):选茧日报(含盈亏/联统率)、选茧计划及其明细、选茧班组及职工。 +/// +public interface IXjrbService +{ + // ---- 选茧日报 ---- + Task> GetPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize); + Task GetByIdAsync(long id); + Task SaveAsync(XjrbSaveDto dto); + Task DeleteAsync(long id); + Task CountYingkuiAsync(XjrbSaveDto dto); + + // ---- 选茧计划 ---- + Task> GetPlanPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize); + Task GetPlanByIdAsync(long id); + Task SavePlanAsync(XjrbPlanSaveDto dto); + Task DeletePlanAsync(long id); + + // ---- 选茧计划明细 ---- + Task> GetPlanItemsAsync(long planId); + Task SavePlanItemAsync(XjrbItemSaveDto dto); + Task DeletePlanItemAsync(long id); + Task CountPlanYingkuiAsync(XjrbItemSaveDto dto); + + // ---- 选茧班组 ---- + Task> GetBanzuPagedAsync(string banci, int? zu, int pageIndex, int pageSize); + Task> GetBanzuZhigongAsync(long banzuId); + Task SaveBanzuZhigongAsync(List dtos); +} + +public class XjrbService : IXjrbService +{ + private readonly IRepository _xjrbRepo; + private readonly IRepository _planRepo; + private readonly IRepository _itemRepo; + private readonly IRepository _banzuRepo; + private readonly IRepository _zgRepo; + private readonly IRepository _gyzkRepo; + + public XjrbService( + IRepository xjrbRepo, IRepository planRepo, IRepository itemRepo, + IRepository banzuRepo, IRepository zgRepo, IRepository gyzkRepo) + { + _xjrbRepo = xjrbRepo; _planRepo = planRepo; _itemRepo = itemRepo; + _banzuRepo = banzuRepo; _zgRepo = zgRepo; _gyzkRepo = gyzkRepo; + } + + private static decimal Round2(decimal v) => Math.Round(v, 2); + + private async Task> GyzkDictAsync() + => (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + + // ==================== 选茧日报 ==================== + + public async Task> GetPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize) + { + var all = await _xjrbRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var gyzkDict = await GyzkDictAsync(); + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.GyzkId > 0 && gyzkDict.ContainsKey(x.GyzkId) && gyzkDict[x.GyzkId].Contains(key)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(ToDto).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + private static XjrbListDto ToDto(XjrbRecord x) => new() + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, + YlLrBaoshu = x.YlLrBaoshu, YlLrZhongliang = x.YlLrZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + LjjBaoshu = x.LjjBaoshu, LjjZhongliang = x.LjjZhongliang, + MjBaoshu = x.MjBaoshu, MjZhongliang = x.MjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + TotalZhongliang = x.TotalZhongliang, + ThBaoshu = x.ThBaoshu, ThZhongliang = x.ThZhongliang, + LiantongRate = x.LiantongRate, Yingkui = x.Yingkui + }; + + public async Task GetByIdAsync(long id) + { + var x = await _xjrbRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧日报不存在", 404); + return new XjrbSaveDto + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, + YlLrBaoshu = x.YlLrBaoshu, YlLrZhongliang = x.YlLrZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + LjjBaoshu = x.LjjBaoshu, LjjZhongliang = x.LjjZhongliang, + MjBaoshu = x.MjBaoshu, MjZhongliang = x.MjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + ThBaoshu = x.ThBaoshu, ThZhongliang = x.ThZhongliang, + LiantongRate = x.LiantongRate, Yingkui = x.Yingkui + }; + } + + public async Task SaveAsync(XjrbSaveDto dto) + { + if (dto.Date == default) throw new ServiceException("请选择选茧日期"); + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + + XjrbRecord entity; + var isNew = dto.Id == 0; + if (isNew) entity = new XjrbRecord(); + else + { + entity = await _xjrbRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("选茧日报不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.GyzkId = dto.GyzkId; + entity.YlLrBaoshu = dto.YlLrBaoshu; + entity.YlLrZhongliang = dto.YlLrZhongliang; + entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang; + entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang; + entity.LjjBaoshu = dto.LjjBaoshu; entity.LjjZhongliang = dto.LjjZhongliang; + entity.MjBaoshu = dto.MjBaoshu; entity.MjZhongliang = dto.MjZhongliang; + entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang; + entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang; + entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang; + entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang; + entity.ThBaoshu = dto.ThBaoshu; entity.ThZhongliang = dto.ThZhongliang; + entity.LiantongRate = dto.LiantongRate; + entity.Yingkui = dto.Yingkui; + // 合计重量 = 8 类茧重量之和(不含退回、不含领入) + entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.LjjZhongliang + + entity.MjZhongliang + entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang); + + if (isNew) await _xjrbRepo.AddAsync(entity); else await _xjrbRepo.UpdateAsync(entity); + return entity.Id; + } + + public async Task DeleteAsync(long id) + { + var x = await _xjrbRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧日报不存在", 404); + await _xjrbRepo.HardDeleteAsync(x); + } + + /// + /// 计算盈亏 / 联统率(对齐 F9 GetXjrbCountYingkuiQuery + GetXjrbLingruStartDateQuery)。 + /// 起始日期:由近及远跳过「联统率 = 0(未结算)」的日报,直到碰到已结算的一条为止; + /// 累计区间 = [起始日期, 当前日期];累加当前录入值 + 区间内历史日报(排除自身)。 + /// 联统率 = 累计联统茧重量 ÷ 累计干茧领入重量 × 100;盈亏 = 累计选出总重量 − 累计干茧领入重量。 + /// + public async Task CountYingkuiAsync(XjrbSaveDto dto) + { + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + + var date = dto.Date.Date; + var all = await _xjrbRepo.GetAllAsync(); + var history = all.Where(x => x.GyzkId == dto.GyzkId && x.Date.Date < date && x.Id != dto.Id) + .OrderByDescending(x => x.Date).ToList(); + + // 步骤 1:往回跳过未结算(LiantongRate == 0)的记录,确定起始日期 + var startDate = date; + foreach (var item in history) + { + if (item.LiantongRate != 0) break; + startDate = item.Date.Date; + } + + // 步骤 2:累计区间(闭区间,排除自身) + var range = history.Where(x => x.Date.Date >= startDate && x.Date.Date <= date).ToList(); + + // 步骤 3:累加(初始值 = 当前表单输入值) + decimal ltj = dto.LtjZhongliang; + decimal yllr = dto.YlLrZhongliang; + decimal allW = dto.LtjZhongliang + dto.HwjjZhongliang + dto.LjjZhongliang + dto.MjZhongliang + + dto.SgjZhongliang + dto.XjjZhongliang + dto.JyZhongliang + dto.JhZhongliang; + foreach (var item in range) + { + ltj += item.LtjZhongliang; + yllr += item.YlLrZhongliang; + allW += item.LtjZhongliang + item.HwjjZhongliang + item.LjjZhongliang + item.MjZhongliang + + item.SgjZhongliang + item.XjjZhongliang + item.JyZhongliang + item.JhZhongliang; + } + + // 步骤 4:公式 + var rate = yllr != 0 ? Round2(ltj / yllr * 100) : 0; + var yingkui = Round2(allW - yllr); + + return new XjrbYingkuiDto { LiantongRate = rate, Yingkui = yingkui, StartDate = startDate }; + } + + // ==================== 选茧计划 ==================== + + public async Task> GetPlanPagedAsync(DateTime? start, DateTime? end, string like, int pageIndex, int pageSize) + { + var all = await _planRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var gyzkDict = await GyzkDictAsync(); + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.GyzkId > 0 && gyzkDict.ContainsKey(x.GyzkId) && gyzkDict[x.GyzkId].Contains(key)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Id) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(ToPlanDto).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + private static XjrbPlanListDto ToPlanDto(XjrbPlan x) => new() + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, YllingliaodanId = x.YllingliaodanId, + YlBaoshu = x.YlBaoshu, YlZhongliang = x.YlZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + TotalZhongliang = x.TotalZhongliang, LiantongRate = x.LiantongRate, + Yingkui = x.Yingkui, IsFinish = x.IsFinish + }; + + public async Task GetPlanByIdAsync(long id) + { + var x = await _planRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧计划不存在", 404); + return new XjrbPlanSaveDto + { + Id = x.Id, Date = x.Date, GyzkId = x.GyzkId, YllingliaodanId = x.YllingliaodanId, + YlBaoshu = x.YlBaoshu, YlZhongliang = x.YlZhongliang, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang + }; + } + + public async Task SavePlanAsync(XjrbPlanSaveDto dto) + { + if (dto.Date == default) throw new ServiceException("请选择日期"); + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + + XjrbPlan entity; + var isNew = dto.Id == 0; + if (isNew) entity = new XjrbPlan(); + else + { + entity = await _planRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("选茧计划不存在", 404); + if (entity.IsFinish) throw new ServiceException("该计划已选茧完成,不能修改"); + } + + entity.Date = dto.Date.Date; + entity.GyzkId = dto.GyzkId; + entity.YllingliaodanId = dto.YllingliaodanId; + entity.YlBaoshu = dto.YlBaoshu; entity.YlZhongliang = dto.YlZhongliang; + entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang; + entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang; + entity.WjjBaoshu = dto.WjjBaoshu; entity.WjjZhongliang = dto.WjjZhongliang; + entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang; + entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang; + entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang; + entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang; + entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.WjjZhongliang + + entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang); + + if (isNew) await _planRepo.AddAsync(entity); else await _planRepo.UpdateAsync(entity); + return entity.Id; + } + + public async Task DeletePlanAsync(long id) + { + var x = await _planRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧计划不存在", 404); + if (x.IsFinish) throw new ServiceException("该计划已选茧完成,不能删除"); + var items = await _itemRepo.FindAsync(i => i.XjrbPlanId == id); + if (items.Count > 0) await _itemRepo.HardDeleteRangeAsync(items); + await _planRepo.HardDeleteAsync(x); + } + + // ==================== 选茧计划明细 ==================== + + public async Task> GetPlanItemsAsync(long planId) + { + var items = await _itemRepo.FindAsync(x => x.XjrbPlanId == planId); + return items.OrderBy(x => x.Date).ThenBy(x => x.Id).Select(x => new XjrbItemDto + { + Id = x.Id, XjrbPlanId = x.XjrbPlanId, Date = x.Date, + LtjBaoshu = x.LtjBaoshu, LtjZhongliang = x.LtjZhongliang, + HwjjBaoshu = x.HwjjBaoshu, HwjjZhongliang = x.HwjjZhongliang, + WjjBaoshu = x.WjjBaoshu, WjjZhongliang = x.WjjZhongliang, + SgjBaoshu = x.SgjBaoshu, SgjZhongliang = x.SgjZhongliang, + XjjBaoshu = x.XjjBaoshu, XjjZhongliang = x.XjjZhongliang, + JyBaoshu = x.JyBaoshu, JyZhongliang = x.JyZhongliang, + JhBaoshu = x.JhBaoshu, JhZhongliang = x.JhZhongliang, + TotalZhongliang = x.TotalZhongliang, IsFinish = x.IsFinish + }).ToList(); + } + + public async Task SavePlanItemAsync(XjrbItemSaveDto dto) + { + if (dto.XjrbPlanId <= 0) throw new ServiceException("计划无效"); + if (dto.Date == default) throw new ServiceException("请选择选茧日期"); + + var plan = await _planRepo.GetByIdAsync(dto.XjrbPlanId); + if (plan == null) throw new ServiceException("选茧计划不存在", 404); + if (plan.IsFinish) throw new ServiceException("该计划已选茧完成,不能修改明细"); + + XjrbItem entity; + var isNew = dto.Id == 0; + if (isNew) entity = new XjrbItem { XjrbPlanId = dto.XjrbPlanId }; + else + { + entity = await _itemRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("选茧明细不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.LtjBaoshu = dto.LtjBaoshu; entity.LtjZhongliang = dto.LtjZhongliang; + entity.HwjjBaoshu = dto.HwjjBaoshu; entity.HwjjZhongliang = dto.HwjjZhongliang; + entity.WjjBaoshu = dto.WjjBaoshu; entity.WjjZhongliang = dto.WjjZhongliang; + entity.SgjBaoshu = dto.SgjBaoshu; entity.SgjZhongliang = dto.SgjZhongliang; + entity.XjjBaoshu = dto.XjjBaoshu; entity.XjjZhongliang = dto.XjjZhongliang; + entity.JyBaoshu = dto.JyBaoshu; entity.JyZhongliang = dto.JyZhongliang; + entity.JhBaoshu = dto.JhBaoshu; entity.JhZhongliang = dto.JhZhongliang; + entity.TotalZhongliang = Round2(entity.LtjZhongliang + entity.HwjjZhongliang + entity.WjjZhongliang + + entity.SgjZhongliang + entity.XjjZhongliang + entity.JyZhongliang + entity.JhZhongliang); + entity.IsFinish = dto.IsFinish; + + if (isNew) await _itemRepo.AddAsync(entity); else await _itemRepo.UpdateAsync(entity); + + // 勾选「选茧完成」时把盈亏回写计划(对齐 F9 UpdateXjrbPlanYingkuiCommand) + if (dto.IsFinish) + { + var yk = await CountPlanYingkuiAsync(dto); + plan.TotalZhongliang = Round2(plan.TotalZhongliang + entity.TotalZhongliang); + plan.Yingkui = yk.Yingkui; + plan.LiantongRate = yk.LiantongRate; + plan.IsFinish = true; + await _planRepo.UpdateAsync(plan); + } + + return entity.Id; + } + + public async Task DeletePlanItemAsync(long id) + { + var x = await _itemRepo.GetByIdAsync(id); + if (x == null) throw new ServiceException("选茧明细不存在", 404); + var plan = await _planRepo.GetByIdAsync(x.XjrbPlanId); + if (plan != null && plan.IsFinish) throw new ServiceException("该计划已选茧完成,不能删除明细"); + await _itemRepo.HardDeleteAsync(x); + } + + /// + /// 计划维度盈亏(对齐 F9 GetXjrbPlanYingkuiQuery,口径与日报不同): + /// 联统率按「包数」:联统茧包数 ÷ 各类茧包数之和 × 100;盈亏 = 累计选出总重量 − 计划原料领入重量。 + /// 注:老系统仓储里 wjjBaoshu / wjjZhongliang 源字段交叉写反(疑似 bug),此处已按正确字段聚合。 + /// + public async Task CountPlanYingkuiAsync(XjrbItemSaveDto dto) + { + var plan = await _planRepo.GetByIdAsync(dto.XjrbPlanId); + if (plan == null) throw new ServiceException("选茧计划不存在", 404); + + var items = (await _itemRepo.FindAsync(x => x.XjrbPlanId == dto.XjrbPlanId && x.Id != dto.Id)).ToList(); + + decimal ltjZ = dto.LtjZhongliang, hwjjZ = dto.HwjjZhongliang, wjjZ = dto.WjjZhongliang, + sgjZ = dto.SgjZhongliang, xjjZ = dto.XjjZhongliang, jyZ = dto.JyZhongliang, jhZ = dto.JhZhongliang; + decimal ltjB = dto.LtjBaoshu, hwjjB = dto.HwjjBaoshu, wjjB = dto.WjjBaoshu, + sgjB = dto.SgjBaoshu, xjjB = dto.XjjBaoshu, jyB = dto.JyBaoshu, jhB = dto.JhBaoshu; + + foreach (var it in items) + { + ltjZ += it.LtjZhongliang; hwjjZ += it.HwjjZhongliang; wjjZ += it.WjjZhongliang; + sgjZ += it.SgjZhongliang; xjjZ += it.XjjZhongliang; jyZ += it.JyZhongliang; jhZ += it.JhZhongliang; + ltjB += it.LtjBaoshu; hwjjB += it.HwjjBaoshu; wjjB += it.WjjBaoshu; + sgjB += it.SgjBaoshu; xjjB += it.XjjBaoshu; jyB += it.JyBaoshu; jhB += it.JhBaoshu; + } + + var totalZhongliang = Round2(ltjZ + hwjjZ + wjjZ + sgjZ + xjjZ + jyZ + jhZ); + var baoshuSum = ltjB + hwjjB + wjjB + sgjB + xjjB + jyB + jhB; + var rate = baoshuSum != 0 ? Round2(ltjB / (decimal)baoshuSum * 100) : 0; + + return new XjrbYingkuiDto { LiantongRate = rate, Yingkui = Round2(totalZhongliang - plan.YlZhongliang), StartDate = plan.Date }; + } + + // ==================== 选茧班组 ==================== + + public async Task> GetBanzuPagedAsync(string banci, int? zu, int pageIndex, int pageSize) + { + var all = await _banzuRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (!string.IsNullOrWhiteSpace(banci)) q = q.Where(x => x.Banci == banci); + if (zu.HasValue) q = q.Where(x => x.Zu == zu.Value); + + var total = q.LongCount(); + var items = q.OrderBy(x => x.Banci).ThenBy(x => x.Zu) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => new XjpbBanzuDto { Id = x.Id, Banci = x.Banci, Zu = x.Zu }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task> GetBanzuZhigongAsync(long banzuId) + { + if (banzuId <= 0) return new List(); + var list = await _zgRepo.FindAsync(x => x.BanzuId == banzuId); + return list.OrderBy(x => x.Sort).ThenBy(x => x.Id).Select(x => new XjpbBanzuZhigongDto + { + Id = x.Id, BanzuId = x.BanzuId, Banci = x.Banci, Zu = x.Zu, + YgId = x.YgId, YgName = x.YgName, YgPhone = x.YgPhone, Sort = x.Sort + }).ToList(); + } + + /// + /// 班组「编辑录入」批量保存(对齐 F9 XjpbBanzuZhigong_SaveCommand): + /// 按班次整体覆盖——先清该班次旧职工与班组,再把「已勾选且有组别」的职工写入,并重建班组。 + /// + public async Task SaveBanzuZhigongAsync(List dtos) + { + var list = dtos ?? new List(); + var banci = list.FirstOrDefault()?.Banci ?? ""; + if (string.IsNullOrWhiteSpace(banci)) throw new ServiceException("班次无效"); + + // 清理该班次旧数据 + var oldZg = await _zgRepo.FindAsync(x => x.Banci == banci); + if (oldZg.Count > 0) await _zgRepo.HardDeleteRangeAsync(oldZg); + var oldBanzu = await _banzuRepo.FindAsync(x => x.Banci == banci); + if (oldBanzu.Count > 0) await _banzuRepo.HardDeleteRangeAsync(oldBanzu); + + var picked = list.Where(x => x.Selected && x.Zu > 0).ToList(); + if (picked.Count == 0) return; + + // 重建班组(班次 + 组 去重) + var banzuDict = new Dictionary(); + foreach (var zu in picked.Select(x => x.Zu).Distinct().OrderBy(x => x)) + { + var banzu = new XjpbBanzu { Banci = banci, Zu = zu }; + await _banzuRepo.AddAsync(banzu); + banzuDict[zu] = banzu.Id; + } + + var sort = 0; + foreach (var p in picked) + { + await _zgRepo.AddAsync(new XjpbBanzuZhigong + { + BanzuId = banzuDict.TryGetValue(p.Zu, out var bid) ? bid : 0, + Banci = banci, Zu = p.Zu, + YgId = p.YgId, YgName = p.YgName ?? "", YgPhone = p.YgPhone ?? "", + Sort = p.Sort > 0 ? p.Sort : ++sort + }); + } + } +} diff --git a/backend/F9WebAiTest.Application/Services/Prod/ZjrbService.cs b/backend/F9WebAiTest.Application/Services/Prod/ZjrbService.cs new file mode 100644 index 0000000..82e9898 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Prod/ZjrbService.cs @@ -0,0 +1,609 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Prod; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Prod; +using F9WebAiTest.Domain.Entities.SYS.Registries; +using F9WebAiTest.Domain.Enums.SYS.Employees; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments; + +namespace F9WebAiTest.Application.Services.Prod; + +/// +/// 煮茧日报(生产-前缫):某日某车间某庄口某规格一条日报,含「机组」「班次」「机组班次」三层明细。 +/// 对应 F9 Page_Zjrb(列表)、Page_Zjrb_EditV2(录入)、Zjrb_SaveCommand / Zjrb_DelCommand / +/// CalculateJizuSaosiHoursCommand(计算缫丝时长)。 +/// +/// 计算口径(与 F9 前端 Page_Zjrb_EditV2 一致,服务端保存时重算): +/// 机组.今缫桶数 = 甲班 + 乙班 +/// 机组.今存桶数 = 上存 + 今缫 − 今落(今落为 0 且余量够一个标准桶时自动补「工艺落丝桶数」) +/// 班次.今缫重量 = 该班次机组今缫桶数 × 每桶茧量 ÷ 1000 +/// 班次.领入/退回重量 = 包数 × 联统茧标重(标重未配置则保留录入值) +/// 班次.今缫包数 = 今缫重量 ÷ 联统茧标重(四舍五入取整) +/// 班次.今存 = 上存 + 领入 − 今缫 − 退回 +/// 主表 = 甲班 + 乙班 +/// +public interface IZjrbService +{ + /// 日报列表(账期起止 + 车间 + 关键字) + Task> GetPagedAsync(DateTime dateFrom, DateTime dateTo, long depId, + string like, int pageIndex, int pageSize); + + /// 某条日报的机组明细(列表页下半表) + Task> GetJizuListAsync(long zjrbId); + + /// 修改回填:主表 + 班次 + 机组 + 机组班次 + Task GetForEditAsync(long id); + + /// 新增:按上一日结存带出上存、每桶茧量、工艺落丝桶数,并按机组车台生成机组行 + Task BuildDefaultsAsync(DateTime date, long depId, long gyzkId, string guige); + + /// 保存(整条替换:主表 upsert + 三层明细先删后建) + Task SaveAsync(ZjrbSaveDto dto); + + /// 删除(四张表级联物理删除) + Task DeleteAsync(long id); + + /// 计算缫丝时长(整日整车间重算:单班 9 小时 / 双班 18 小时,按茧量比例分配) + Task CalculateJizuSaosiHoursAsync(DateTime date, long depId); +} + +public class ZjrbService : IZjrbService +{ + /// 单班 / 双班的机组标准时长 + private const decimal SingleBanciHours = 9m; + private const decimal TwoBanciHours = 18m; + + /// 联统茧标重的参数配置位置(与 F9 一致) + private const string RegistryCategory = @"生产\前缫\煮茧"; + private const string RegistryKey = "联统茧标重"; + + private readonly IRepository _zjrbRepo; + private readonly IRepository _jizuRepo; + private readonly IRepository _banciRepo; + private readonly IRepository _jizuBanciRepo; + private readonly IRepository _jzctRepo; + private readonly IRepository _cjbcRepo; + private readonly IRepository _gyzkRepo; + private readonly IRepository _registryRepo; + private readonly IRepository _deptRepo; + + public ZjrbService(IRepository zjrbRepo, IRepository jizuRepo, + IRepository banciRepo, IRepository jizuBanciRepo, + IRepository jzctRepo, IRepository cjbcRepo, + IRepository gyzkRepo, IRepository registryRepo, + IRepository deptRepo) + { + _zjrbRepo = zjrbRepo; + _jizuRepo = jizuRepo; + _banciRepo = banciRepo; + _jizuBanciRepo = jizuBanciRepo; + _jzctRepo = jzctRepo; + _cjbcRepo = cjbcRepo; + _gyzkRepo = gyzkRepo; + _registryRepo = registryRepo; + _deptRepo = deptRepo; + } + + // ==================== 查询 ==================== + + public async Task> GetPagedAsync(DateTime dateFrom, DateTime dateTo, long depId, + string like, int pageIndex, int pageSize) + { + if (pageIndex <= 0) pageIndex = 1; + if (pageSize <= 0) pageSize = 20; + + var from = dateFrom.Date; + var to = dateTo.Date; + if (to < from) (from, to) = (to, from); + + var ctx = await BuildContextAsync(); + + var rows = await _zjrbRepo.FindAsync(x => x.Date >= from && x.Date <= to); + if (depId > 0) rows = rows.Where(x => x.DeptId == depId).ToList(); + + if (!string.IsNullOrWhiteSpace(like)) + { + var kw = like.Trim(); + rows = rows.Where(x => + ctx.GyzkNames.GetValueOrDefault(x.GyzkId, "").Contains(kw, StringComparison.OrdinalIgnoreCase) + || (x.Guige ?? "").Contains(kw, StringComparison.OrdinalIgnoreCase)) + .ToList(); + } + + var total = rows.Count; + var pageRows = rows + .OrderByDescending(x => x.Date) + .ThenBy(x => ctx.DeptNames.GetValueOrDefault(x.DeptId, "")) + .ThenBy(x => x.GyzkId) + .ThenBy(x => x.Guige) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + var ids = pageRows.Select(x => x.Id).ToList(); + var jizus = ids.Count == 0 + ? new List() + : await _jizuRepo.FindAsync(x => ids.Contains(x.ZjrbId)); + + var items = pageRows.Select(x => + { + var jz = jizus.Where(j => j.ZjrbId == x.Id).ToList(); + return new ZjrbReadDto_View + { + Id = x.Id, + Date = x.Date, + DepId = x.DeptId, + DepName = ctx.DeptNames.GetValueOrDefault(x.DeptId, ""), + GyzkId = x.GyzkId, + GyzkName = ctx.GyzkNames.GetValueOrDefault(x.GyzkId, ""), + Guige = x.Guige ?? "", + LastDate = x.LastDate, + StandardLuositongshu = x.StandardLuositongshu, + MeitongJianliang = x.MeitongJianliang, + BaoshuShangcun = x.BaoshuShangcun, + BaoshuLingru = x.BaoshuLingru, + BaoshuSaosi = x.BaoshuSaosi, + BaoshuTuihui = x.BaoshuTuihui, + BaoshuJincun = x.BaoshuJincun, + JianliangShangcun = x.JianliangShangcun, + JianliangLingru = x.JianliangLingru, + JianliangSaosi = x.JianliangSaosi, + JianliangTuihui = x.JianliangTuihui, + JianliangJincun = x.JianliangJincun, + TongshuShangcun = jz.Sum(j => j.TongshuShangcun), + TongshuJinsao = jz.Sum(j => j.TongshuJinsao), + TongshuJinluo = jz.Sum(j => j.TongshuJinluo), + TongshuJincun = jz.Sum(j => j.TongshuJincun), + JizuSaosiHours = jz.Sum(j => j.JizuSaosiHours) + }; + }).ToList(); + + return new PageResult + { + Items = items, + Total = total, + PageIndex = pageIndex, + PageSize = pageSize + }; + } + + public async Task> GetJizuListAsync(long zjrbId) + { + var jizus = (await _jizuRepo.FindAsync(x => x.ZjrbId == zjrbId)).OrderBy(x => x.Jizu).ToList(); + var jb = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == zjrbId); + return jizus.Select(j => ToJizuDto(j, jb)).ToList(); + } + + public async Task GetForEditAsync(long id) + { + var master = await _zjrbRepo.GetByIdAsync(id); + if (master == null) throw new ServiceException("煮茧日报不存在或已删除"); + + var ctx = await BuildContextAsync(); + var banciRows = (await _banciRepo.FindAsync(x => x.ZjrbId == id)).OrderBy(x => x.Banci).ToList(); + var jizuRows = (await _jizuRepo.FindAsync(x => x.ZjrbId == id)).OrderBy(x => x.Jizu).ToList(); + var jbRows = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == id); + + var dto = new ZjrbSaveDto + { + Id = master.Id, + Date = master.Date, + DepId = master.DeptId, + DepName = ctx.DeptNames.GetValueOrDefault(master.DeptId, ""), + GyzkId = master.GyzkId, + GyzkName = ctx.GyzkNames.GetValueOrDefault(master.GyzkId, ""), + Guige = master.Guige ?? "", + LastDate = master.LastDate, + StandardLuositongshu = master.StandardLuositongshu, + MeitongJianliang = master.MeitongJianliang, + LiantongBiaozhong = await GetLiantongBiaozhongAsync(), + BaoshuShangcun = master.BaoshuShangcun, + BaoshuLingru = master.BaoshuLingru, + BaoshuSaosi = master.BaoshuSaosi, + BaoshuTuihui = master.BaoshuTuihui, + BaoshuJincun = master.BaoshuJincun, + JianliangShangcun = master.JianliangShangcun, + JianliangLingru = master.JianliangLingru, + JianliangSaosi = master.JianliangSaosi, + JianliangTuihui = master.JianliangTuihui, + JianliangJincun = master.JianliangJincun, + Bancis = banciRows.Select(ToBanciDto).ToList(), + Jizus = jizuRows.Select(j => ToJizuDto(j, jbRows)).ToList() + }; + + // 历史数据可能缺班次行(或车间班次配置调整过),按配置补齐甲乙班 + var need = (await GetBanciKindsAsync(master.DeptId)); + foreach (var b in need) + { + if (dto.Bancis.All(x => x.Banci != b)) + dto.Bancis.Add(new ZjrbBanciDto { Banci = b, BanciName = b.ToString() }); + } + dto.Bancis = dto.Bancis.OrderBy(x => x.Banci).ToList(); + + return dto; + } + + public async Task BuildDefaultsAsync(DateTime date, long depId, long gyzkId, string guige) + { + if (depId <= 0) throw new ServiceException("请选择车间"); + + var ctx = await BuildContextAsync(); + var d = date.Date; + var g = (guige ?? string.Empty).Trim(); + + // 上一日:同车间 + 同庄口(+ 同规格)的最近一条日报 + var candidates = await _zjrbRepo.FindAsync(x => x.Date < d && x.DeptId == depId && x.GyzkId == gyzkId); + var last = candidates + .Where(x => string.IsNullOrEmpty(g) || (x.Guige ?? "") == g) + .OrderByDescending(x => x.Date) + .FirstOrDefault(); + + var dto = new ZjrbSaveDto + { + Date = d, + DepId = depId, + DepName = ctx.DeptNames.GetValueOrDefault(depId, ""), + GyzkId = gyzkId, + GyzkName = ctx.GyzkNames.GetValueOrDefault(gyzkId, ""), + Guige = g, + LiantongBiaozhong = await GetLiantongBiaozhongAsync() + }; + + List lastBancis = new(); + List lastJizus = new(); + if (last != null) + { + dto.LastDate = last.Date; + dto.StandardLuositongshu = last.StandardLuositongshu; + dto.MeitongJianliang = last.MeitongJianliang; + lastBancis = await _banciRepo.FindAsync(x => x.ZjrbId == last.Id); + lastJizus = await _jizuRepo.FindAsync(x => x.ZjrbId == last.Id); + } + + // 班次行:按车间班次配置;未配置则固定甲乙两班 + foreach (var b in await GetBanciKindsAsync(depId)) + { + var prev = lastBancis.FirstOrDefault(x => x.Banci == b); + dto.Bancis.Add(new ZjrbBanciDto + { + Banci = b, + BanciName = b.ToString(), + BaoshuShangcun = prev?.BaoshuJincun ?? 0, + JianliangShangcun = prev?.JianliangJincun ?? 0 + }); + } + + // 机组行:按机组车台配置;上存桶数取上一日该机组今存桶数 + var jizus = (await _jzctRepo.FindAsync(x => x.DepartmentId == depId)) + .Select(x => x.Jizu).Distinct().OrderBy(x => x).ToList(); + foreach (var jz in jizus) + { + var prev = lastJizus.FirstOrDefault(x => x.Jizu == jz); + dto.Jizus.Add(new ZjrbJizuDto + { + Jizu = jz, + TongshuShangcun = prev?.TongshuJincun ?? 0 + }); + } + + return dto; + } + + // ==================== 保存 / 删除 ==================== + + public async Task SaveAsync(ZjrbSaveDto dto) + { + if (dto == null) throw new ServiceException("参数不能为空"); + if (dto.Date == default) throw new ServiceException("请选择日期"); + if (dto.DepId <= 0) throw new ServiceException("请选择车间"); + if (dto.GyzkId <= 0) throw new ServiceException("请选择工艺庄口"); + if (dto.MeitongJianliang <= 0) throw new ServiceException("每桶茧量必须大于 0"); + if (dto.StandardLuositongshu <= 0) throw new ServiceException("工艺落丝桶数必须大于 0"); + if (dto.Bancis == null || dto.Bancis.Count == 0) throw new ServiceException("请至少保留一个班次行"); + + var d = dto.Date.Date; + var guige = (dto.Guige ?? string.Empty).Trim(); + + // 唯一性:同 日期 + 车间 + 庄口 + 规格 + var exists = await _zjrbRepo.AnyAsync(x => + x.Id != dto.Id && x.Date == d && x.DeptId == dto.DepId && x.GyzkId == dto.GyzkId && x.Guige == guige); + if (exists) throw new ServiceException("已存在相同的煮茧日报!"); + + Normalize(dto, guige); + + long id = dto.Id; + if (id > 0) + { + var master = await _zjrbRepo.GetByIdAsync(id); + if (master == null) throw new ServiceException("煮茧日报不存在或已删除"); + CopyMaster(dto, master); + await _zjrbRepo.UpdateAsync(master); + await DeleteChildrenAsync(id); + } + else + { + var master = new ZjrbRecord { LastDate = dto.LastDate }; + CopyMaster(dto, master); + await _zjrbRepo.AddAsync(master); + id = master.Id; + } + + // 班次 + foreach (var b in dto.Bancis) + { + await _banciRepo.AddAsync(new ZjrbBanci + { + ZjrbId = id, + Banci = b.Banci, + BaoshuShangcun = b.BaoshuShangcun, + BaoshuLingru = b.BaoshuLingru, + BaoshuSaosi = b.BaoshuSaosi, + BaoshuTuihui = b.BaoshuTuihui, + BaoshuJincun = b.BaoshuJincun, + JianliangShangcun = b.JianliangShangcun, + JianliangLingru = b.JianliangLingru, + JianliangSaosi = b.JianliangSaosi, + JianliangTuihui = b.JianliangTuihui, + JianliangJincun = b.JianliangJincun + }); + } + + // 机组 + 机组班次(机组班次挂在刚生成的机组 Id 上) + foreach (var j in dto.Jizus) + { + var jizu = await _jizuRepo.AddAsync(new ZjrbJizu + { + ZjrbId = id, + Jizu = j.Jizu, + JizuSaosiHours = j.JizuSaosiHours, + TongshuShangcun = j.TongshuShangcun, + TongshuJinsao = j.TongshuJinsaoJiaban + j.TongshuJinsaoYiban, + TongshuJinluo = j.TongshuJinluo, + TongshuJincun = j.TongshuJincun + }); + + foreach (var b in dto.Bancis) + { + await _jizuBanciRepo.AddAsync(new ZjrbJizuBanci + { + ZjrbId = id, + ZjrbJizuId = jizu.Id, + Jizu = j.Jizu, + Banci = b.Banci, + TongshuJinsao = b.Banci == Prod_Banci_Enum.甲 ? j.TongshuJinsaoJiaban : j.TongshuJinsaoYiban + }); + } + } + + await _zjrbRepo.SaveChangesAsync(); + return id; + } + + public async Task DeleteAsync(long id) + { + var master = await _zjrbRepo.GetByIdAsync(id); + if (master == null) throw new ServiceException("煮茧日报不存在或已删除"); + await DeleteChildrenAsync(id); + await _zjrbRepo.HardDeleteAsync(master); + } + + private async Task DeleteChildrenAsync(long zjrbId) + { + var jizuRows = await _jizuRepo.FindAsync(x => x.ZjrbId == zjrbId); + if (jizuRows.Count > 0) await _jizuRepo.HardDeleteRangeAsync(jizuRows); + + var banciRows = await _banciRepo.FindAsync(x => x.ZjrbId == zjrbId); + if (banciRows.Count > 0) await _banciRepo.HardDeleteRangeAsync(banciRows); + + var jbRows = await _jizuBanciRepo.FindAsync(x => x.ZjrbId == zjrbId); + if (jbRows.Count > 0) await _jizuBanciRepo.HardDeleteRangeAsync(jbRows); + } + + // ==================== 计算缫丝时长 ==================== + + public async Task CalculateJizuSaosiHoursAsync(DateTime date, long depId) + { + if (depId <= 0) throw new ServiceException("请选择车间"); + var d = date.Date; + + var reports = await _zjrbRepo.FindAsync(x => x.Date == d && x.DeptId == depId); + if (reports.Count == 0) throw new ServiceException("该日该车间没有煮茧日报,无法计算缫丝时长"); + + var reportMap = reports.ToDictionary(x => x.Id); + var ids = reportMap.Keys.ToList(); + var jizus = await _jizuRepo.FindAsync(x => ids.Contains(x.ZjrbId)); + if (jizus.Count == 0) throw new ServiceException("该日该车间没有机组明细,无法计算缫丝时长"); + var jbRows = await _jizuBanciRepo.FindAsync(x => ids.Contains(x.ZjrbId)); + + // 双班判据:该机组「今缫桶数 > 0」的班次个数为 2 → 18 小时,否则 9 小时 + var hasTwoBanci = jizus.Select(x => x.Jizu).Distinct().ToDictionary( + jz => jz, + jz => jbRows.Where(x => x.Jizu == jz && x.TongshuJinsao > 0) + .Select(x => x.Banci).Distinct().Count() == 2); + + // 机组总茧量 = Σ(每桶茧量 × 机组今缫桶数) + var totalJianliang = jizus.GroupBy(x => x.Jizu).ToDictionary( + g => g.Key, + g => g.Sum(x => (decimal)reportMap[x.ZjrbId].MeitongJianliang * x.TongshuJinsao)); + + var changed = 0; + var groups = jizus.GroupBy(x => new + { + reportMap[x.ZjrbId].GyzkId, + Guige = reportMap[x.ZjrbId].Guige ?? "", + x.Jizu + }); + + foreach (var group in groups) + { + var jz = group.Key.Jizu; + var totalHours = hasTwoBanci.GetValueOrDefault(jz) ? TwoBanciHours : SingleBanciHours; + var jizuTotal = totalJianliang.GetValueOrDefault(jz); + var current = group.Sum(x => (decimal)reportMap[x.ZjrbId].MeitongJianliang * x.TongshuJinsao); + var ratio = jizuTotal > 0 ? current / jizuTotal : 0m; + var newHours = Math.Round(ratio * totalHours, 1, MidpointRounding.AwayFromZero); + + foreach (var item in group) + { + if (item.JizuSaosiHours == newHours) continue; + item.JizuSaosiHours = newHours; + await _jizuRepo.UpdateAsync(item); + changed++; + } + } + + return changed; + } + + // ==================== 内部:口径重算 / 上下文 ==================== + + /// 按 F9 编辑页口径重算派生字段(机组今存、班次重量与今存、主表合计) + private static void Normalize(ZjrbSaveDto dto, string guige) + { + dto.Guige = guige; + + foreach (var j in dto.Jizus) + { + if (j.JizuSaosiHours < 0) throw new ServiceException($"机组{j.Jizu}的缫丝时长不能为负数"); + if (j.JizuSaosiHours > 20) throw new ServiceException($"机组{j.Jizu}缫丝时长不能大于 20"); + j.TongshuJinsao = j.TongshuJinsaoJiaban + j.TongshuJinsaoYiban; + if (j.TongshuJinsao > 0 && j.JizuSaosiHours <= 0) + throw new ServiceException($"机组{j.Jizu}今缫桶数大于 0 时,请输入缫丝时长"); + + var pre = j.TongshuShangcun + j.TongshuJinsao - j.TongshuJinluo; + if (j.TongshuJinluo == 0 && dto.StandardLuositongshu > 0 && pre >= dto.StandardLuositongshu) + { + j.TongshuJinluo = dto.StandardLuositongshu; + pre -= dto.StandardLuositongshu; + } + j.TongshuJincun = pre; + } + + foreach (var b in dto.Bancis) + { + if (b.BaoshuShangcun > 0 && b.JianliangShangcun <= 0) + throw new ServiceException($"{b.Banci}班上存包数大于 0 时,请填写上存重量"); + + var tongshu = dto.Jizus.Sum(j => b.Banci == Prod_Banci_Enum.甲 + ? j.TongshuJinsaoJiaban + : j.TongshuJinsaoYiban); + + b.JianliangSaosi = Math.Round(tongshu * (decimal)dto.MeitongJianliang / 1000m, 1, + MidpointRounding.AwayFromZero); + + if (dto.LiantongBiaozhong > 0) + { + b.JianliangLingru = Math.Round(b.BaoshuLingru * dto.LiantongBiaozhong, 1, + MidpointRounding.AwayFromZero); + b.JianliangTuihui = Math.Round(b.BaoshuTuihui * dto.LiantongBiaozhong, 1, + MidpointRounding.AwayFromZero); + b.BaoshuSaosi = (int)Math.Round(b.JianliangSaosi / dto.LiantongBiaozhong, 0, + MidpointRounding.AwayFromZero); + } + + b.JianliangJincun = b.JianliangShangcun + b.JianliangLingru - b.JianliangSaosi - b.JianliangTuihui; + b.BaoshuJincun = b.BaoshuShangcun + b.BaoshuLingru - b.BaoshuSaosi - b.BaoshuTuihui; + } + + // 主表 = 甲班 + 乙班 + dto.BaoshuShangcun = dto.Bancis.Sum(x => x.BaoshuShangcun); + dto.BaoshuLingru = dto.Bancis.Sum(x => x.BaoshuLingru); + dto.BaoshuSaosi = dto.Bancis.Sum(x => x.BaoshuSaosi); + dto.BaoshuTuihui = dto.Bancis.Sum(x => x.BaoshuTuihui); + dto.BaoshuJincun = dto.Bancis.Sum(x => x.BaoshuJincun); + dto.JianliangShangcun = dto.Bancis.Sum(x => x.JianliangShangcun); + dto.JianliangLingru = dto.Bancis.Sum(x => x.JianliangLingru); + dto.JianliangSaosi = dto.Bancis.Sum(x => x.JianliangSaosi); + dto.JianliangTuihui = dto.Bancis.Sum(x => x.JianliangTuihui); + dto.JianliangJincun = dto.Bancis.Sum(x => x.JianliangJincun); + } + + private static void CopyMaster(ZjrbSaveDto dto, ZjrbRecord master) + { + master.Date = dto.Date.Date; + master.DeptId = dto.DepId; + master.GyzkId = dto.GyzkId; + master.Guige = dto.Guige ?? ""; + master.LastDate = dto.LastDate; + master.StandardLuositongshu = dto.StandardLuositongshu; + master.MeitongJianliang = dto.MeitongJianliang; + master.BaoshuShangcun = dto.BaoshuShangcun; + master.BaoshuLingru = dto.BaoshuLingru; + master.BaoshuSaosi = dto.BaoshuSaosi; + master.BaoshuTuihui = dto.BaoshuTuihui; + master.BaoshuJincun = dto.BaoshuJincun; + master.JianliangShangcun = dto.JianliangShangcun; + master.JianliangLingru = dto.JianliangLingru; + master.JianliangSaosi = dto.JianliangSaosi; + master.JianliangTuihui = dto.JianliangTuihui; + master.JianliangJincun = dto.JianliangJincun; + } + + private static ZjrbBanciDto ToBanciDto(ZjrbBanci x) => new() + { + Id = x.Id, + ZjrbId = x.ZjrbId, + Banci = x.Banci, + BanciName = x.Banci.ToString(), + BaoshuShangcun = x.BaoshuShangcun, + BaoshuLingru = x.BaoshuLingru, + BaoshuSaosi = x.BaoshuSaosi, + BaoshuTuihui = x.BaoshuTuihui, + BaoshuJincun = x.BaoshuJincun, + JianliangShangcun = x.JianliangShangcun, + JianliangLingru = x.JianliangLingru, + JianliangSaosi = x.JianliangSaosi, + JianliangTuihui = x.JianliangTuihui, + JianliangJincun = x.JianliangJincun + }; + + private static ZjrbJizuDto ToJizuDto(ZjrbJizu x, List jbRows) => new() + { + Id = x.Id, + ZjrbId = x.ZjrbId, + Jizu = x.Jizu, + JizuSaosiHours = x.JizuSaosiHours, + TongshuShangcun = x.TongshuShangcun, + TongshuJinsaoJiaban = jbRows.FirstOrDefault(b => b.ZjrbJizuId == x.Id && b.Banci == Prod_Banci_Enum.甲) + ?.TongshuJinsao ?? 0, + TongshuJinsaoYiban = jbRows.FirstOrDefault(b => b.ZjrbJizuId == x.Id && b.Banci == Prod_Banci_Enum.乙) + ?.TongshuJinsao ?? 0, + TongshuJinsao = x.TongshuJinsao, + TongshuJinluo = x.TongshuJinluo, + TongshuJincun = x.TongshuJincun + }; + + /// 车间班次配置;未配置时固定甲乙两班 + private async Task> GetBanciKindsAsync(long depId) + { + var cfgs = await _cjbcRepo.FindAsync(x => x.DeptId == depId); + if (cfgs.Count == 0) return new List { Prod_Banci_Enum.甲, Prod_Banci_Enum.乙 }; + return cfgs.Select(x => x.Banci).Distinct().OrderBy(x => x).ToList(); + } + + private async Task GetLiantongBiaozhongAsync() + { + var row = await _registryRepo.FirstOrDefaultAsync(x => + x.Category == RegistryCategory && x.KeyName == RegistryKey); + if (row == null || string.IsNullOrWhiteSpace(row.Value)) return 0m; + return decimal.TryParse(row.Value, out var v) ? v : 0m; + } + + private sealed class Ctx + { + public Dictionary DeptNames { get; init; } = new(); + public Dictionary GyzkNames { get; init; } = new(); + } + + private async Task BuildContextAsync() + { + var depts = await _deptRepo.GetAllAsync(); + var gyzks = await _gyzkRepo.GetAllAsync(); + return new Ctx + { + DeptNames = depts.ToDictionary(d => d.Id, d => d.Name), + GyzkNames = gyzks.ToDictionary(z => z.Id, z => z.ProductionDisplayName is { Length: > 0 } n + ? n + : z.ZhuangKouName) + }; + } +} diff --git a/backend/F9WebAiTest.Application/Services/Wms/WmsService.cs b/backend/F9WebAiTest.Application/Services/Wms/WmsService.cs new file mode 100644 index 0000000..94daf99 --- /dev/null +++ b/backend/F9WebAiTest.Application/Services/Wms/WmsService.cs @@ -0,0 +1,1903 @@ +using F9WebAiTest.Application.Common; +using F9WebAiTest.Application.Dtos.Wms; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Entities.Base; +using F9WebAiTest.Domain.Entities.Wms; +using SysDept = F9WebAiTest.Domain.Entities.SYS.Departments.Department; +using SysGys = F9WebAiTest.Domain.Entities.SYS.Gongyingshangs.Gongyingshang; +using SysWz = F9WebAiTest.Domain.Entities.SYS.WuziMulus.WuziMulu; +using SysYlzk = F9WebAiTest.Domain.Entities.SYS.YuanliaoZhuangkous.YuanliaoZhuangkou; +using WuziGroup = F9WebAiTest.Domain.Entities.SYS.WuziMulus.WuziGroup; + +namespace F9WebAiTest.Application.Services.Wms; + +/// +/// 仓储(WMS)统一服务:原料 / 副产品 / 在制品 / 生丝 / 物资 五个模块。 +/// 对齐 F9 WinClient\F9.Business.WMS:入库、出库、库存台账(在制品另有笼筒架台账), +/// 以及各模块扩展:原料采购 / 翻包计划、生丝批次包件、物资库存 / 出入库流水 / 采购计划 / 采购报销。 +/// 五个模块结构一致,用 Module(Wms_Module_Enum)+ Kind(0库存台账 1笼筒架台账)区分。 +/// +public interface IWmsService +{ + Task GetOptionsAsync(int module); + + Task> GetRukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize); + Task GetRukuAsync(long id); + Task SaveRukuAsync(WmsRukuSaveDto dto); + Task DeleteRukuAsync(long id); + Task BaoxiaoAsync(List rukuIds); + + Task> GetChukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize); + Task GetChukuAsync(long id); + Task SaveChukuAsync(WmsChukuSaveDto dto); + Task DeleteChukuAsync(long id); + + Task> GetTzDatesAsync(int module, int kind); + Task GetTaizhangAsync(int module, int kind, DateTime date, bool regenerate); + Task SaveTaizhangAsync(WmsTzSaveDto dto); + Task DeleteTaizhangAsync(long id); + + Task> GetKucunAsync(int module, string like); + Task> GetCrkAsync(int module, DateTime start, DateTime end, long? wzmlId); + + Task> GetPiciPagedAsync(DateTime? start, DateTime? end, int kcStatus, string like, int pageIndex, int pageSize); + Task GetPiciAsync(long id); + Task SavePiciAsync(WmsPiciSaveDto dto); + Task DeletePiciAsync(long id); + Task> GetPiciChukuSelectAsync(long wzmlId, string excludePiciIds); + Task GetPiciQcAsync(long piciId); + Task SavePiciQcAsync(WmsPiciQcDto dto); + Task> GetPiciQcXdjyAsync(long piciId); + Task SavePiciQcXdjyAsync(List items); + + Task> GetCaigouAsync(DateTime? start, DateTime? end); + Task SaveCaigouAsync(WmsCaigouDto dto); + Task DeleteCaigouAsync(long id); + + Task> GetFbplanAsync(DateTime? start, DateTime? end); + Task SaveFbplanAsync(WmsFbplanDto dto); + Task DeleteFbplanAsync(long id); + + Task> GetCgplanAsync(DateTime? start, DateTime? end); + Task GetCgplanAsync(long id); + Task SaveCgplanAsync(WmsCgplanSaveDto dto); + Task DeleteCgplanAsync(long id); +} + +public class WmsService : IWmsService +{ + private readonly IRepository _rkRepo; + private readonly IRepository _rkDetailRepo; + private readonly IRepository _bmdRepo; + private readonly IRepository _ckRepo; + private readonly IRepository _ckDetailRepo; + private readonly IRepository _tzDateRepo; + private readonly IRepository _tzItemRepo; + private readonly IRepository _piciRepo; + private readonly IRepository _baojianRepo; + private readonly IRepository _piciChukuRepo; + private readonly IRepository _piciQcRepo; + private readonly IRepository _piciQcXdjyRepo; + private readonly IRepository _kucunRepo; + private readonly IRepository _caigouRepo; + private readonly IRepository _fbplanRepo; + private readonly IRepository _fbjlRepo; + private readonly IRepository _cgplanRepo; + private readonly IRepository _cgplanItemRepo; + private readonly IRepository _wzRepo; + private readonly IRepository _wzGroupRepo; + private readonly IRepository _ylzkRepo; + private readonly IRepository _gysRepo; + private readonly IRepository _depRepo; + private readonly IRepository _gyzkRepo; + + public WmsService( + IRepository rkRepo, IRepository rkDetailRepo, IRepository bmdRepo, + IRepository ckRepo, IRepository ckDetailRepo, + IRepository tzDateRepo, IRepository tzItemRepo, + IRepository piciRepo, IRepository baojianRepo, + IRepository piciChukuRepo, IRepository piciQcRepo, IRepository piciQcXdjyRepo, + IRepository kucunRepo, + IRepository caigouRepo, IRepository fbplanRepo, IRepository fbjlRepo, + IRepository cgplanRepo, IRepository cgplanItemRepo, + IRepository wzRepo, IRepository wzGroupRepo, IRepository ylzkRepo, + IRepository gysRepo, IRepository depRepo, IRepository gyzkRepo) + { + _rkRepo = rkRepo; _rkDetailRepo = rkDetailRepo; _bmdRepo = bmdRepo; + _ckRepo = ckRepo; _ckDetailRepo = ckDetailRepo; + _tzDateRepo = tzDateRepo; _tzItemRepo = tzItemRepo; + _piciRepo = piciRepo; _baojianRepo = baojianRepo; + _piciChukuRepo = piciChukuRepo; _piciQcRepo = piciQcRepo; _piciQcXdjyRepo = piciQcXdjyRepo; + _kucunRepo = kucunRepo; + _caigouRepo = caigouRepo; _fbplanRepo = fbplanRepo; _fbjlRepo = fbjlRepo; + _cgplanRepo = cgplanRepo; _cgplanItemRepo = cgplanItemRepo; + _wzRepo = wzRepo; _wzGroupRepo = wzGroupRepo; _ylzkRepo = ylzkRepo; + _gysRepo = gysRepo; _depRepo = depRepo; _gyzkRepo = gyzkRepo; + } + + // ==================== 模块元数据(对齐 F9 枚举) ==================== + + private static readonly Dictionary ModuleTypes = new() + { + // 原料:YLRk_Type 采购1/调拨2/其他3;YLCk_Type 领料1/销售2/样茧3/其他4 + { 1, (new[] { 1, 2, 3 }, new[] { "采购", "调拨", "其他" }, new[] { 1, 2, 3, 4 }, new[] { "领料", "销售", "样茧", "其他" }) }, + // 副产品:FcpRk_Type 生产1/试缫2/其他5;FcpCk_Type 销售1/调拨2/其他3 + { 2, (new[] { 1, 2, 5 }, new[] { "生产", "试缫", "其他" }, new[] { 1, 2, 3 }, new[] { "销售", "调拨", "其他" }) }, + // 在制品:ZJP_Rk_Type 选茧0/试缫2/退料3/其他5;ZJP_Ck_Type 领料0/销售1/其他2 + { 3, (new[] { 0, 2, 3, 5 }, new[] { "选茧", "试缫", "退料", "其他" }, new[] { 0, 1, 2 }, new[] { "领料", "销售", "其他" }) }, + // 生丝:SSRk_Type 生产0/退货1/其他2;SSCk_Type 销售1/其他5 + { 4, (new[] { 0, 1, 2 }, new[] { "生产", "退货", "其他" }, new[] { 1, 5 }, new[] { "销售", "其他" }) }, + // 物资:WJRk_Type 采购1/冲账2/盘盈3/其他4;WJCk_Type 领料1/销售2/调拨3/报废4/其他5 + { 5, (new[] { 1, 2, 3, 4 }, new[] { "采购", "冲账", "盘盈", "其他" }, new[] { 1, 2, 3, 4, 5 }, new[] { "领料", "销售", "调拨", "报废", "其他" }) } + }; + + private static string RkTypeName(int module, int type) + { + if (!ModuleTypes.TryGetValue(module, out var t)) return type.ToString(); + var i = Array.IndexOf(t.Rk, type); + return i >= 0 ? t.RkNames[i] : type.ToString(); + } + + private static string CkTypeName(int module, int type) + { + if (!ModuleTypes.TryGetValue(module, out var t)) return type.ToString(); + var i = Array.IndexOf(t.Ck, type); + return i >= 0 ? t.CkNames[i] : type.ToString(); + } + + /// 台账项目口径:原料=原料庄口;笼筒架=工艺庄口;其余=物资目录 + private static bool IsYlzkItem(int module, int kind) => module == (int)Wms_Module_Enum.原料 && kind == 0; + private static bool IsGyzkItem(int module, int kind) => module == (int)Wms_Module_Enum.中间品 && kind == 1; + + /// 台账两列口径:物资=数量/金额;其余=件数/净重 + private static bool IsWujizl(int module) => module == (int)Wms_Module_Enum.物资; + + /// 仓储模块 → 物资分组的业务类型(原料1 / 中间品2 / 生丝3 / 副产品4 / 五金5) + private static int WuziBizTypeOfModule(int module) => module switch + { + (int)Wms_Module_Enum.原料 => 1, + (int)Wms_Module_Enum.中间品 => 2, + (int)Wms_Module_Enum.生丝 => 3, + (int)Wms_Module_Enum.副产品 => 4, + _ => 5 + }; + + private static string BxStatusName(int v) => v switch { 1 => "待报销", 2 => "已报销", _ => "无" }; + private static string RkStatusName(int v) => v == 1 ? "已入库" : "未入库"; + private static string CkStatusName(int v) => v == 1 ? "已出库" : "未出库"; + private static string FbTypeName(int v) => v switch { 1 => "翻包中", 2 => "完成", _ => "暂停" }; + private static string RukuStatusName(int v) => v switch { 1 => "有效", 2 => "作废", _ => "无" }; + private static string KcStatusName(int v) => v switch { 1 => "待入库", 2 => "已入库", 3 => "部分出库", 4 => "已出库", _ => "全部" }; + private static string CgplanStatusName(int v) => v switch { 1 => "已确认", 2 => "已完成", _ => "草稿" }; + + // ==================== 基础数据 ==================== + + public async Task GetOptionsAsync(int module) + { + var result = new WmsOptionsDto(); + if (ModuleTypes.TryGetValue(module, out var t)) + { + for (var i = 0; i < t.Rk.Length; i++) result.RukuTypes.Add(new WmsTypeOptionDto { Value = t.Rk[i], Name = t.RkNames[i] }); + for (var i = 0; i < t.Ck.Length; i++) result.ChukuTypes.Add(new WmsTypeOptionDto { Value = t.Ck[i], Name = t.CkNames[i] }); + } + + // 各仓储模块只加载自己对应的物资分组(物资分组 BusinessType:原料1/中间品2/生丝3/副产品4/五金5) + var bizType = WuziBizTypeOfModule(module); + var groupIds = (await _wzGroupRepo.GetAllAsync()) + .Where(g => (int)g.BusinessType == bizType).Select(g => g.Id).ToHashSet(); + var wzs = (await _wzRepo.GetAllAsync()).Where(x => groupIds.Contains(x.GroupId)).ToList(); + result.Wuzi = wzs.OrderBy(x => x.Code).Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name, Code = x.Code, Ext = x.Unit }).ToList(); + + var ylzks = await _ylzkRepo.GetAllAsync(); + result.YuanliaoZhuangkou = ylzks.OrderByDescending(x => x.Year).ThenBy(x => x.Code) + .Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name, Code = x.Code }).ToList(); + + var gyzks = await _gyzkRepo.GetAllAsync(); + result.CraftZhuangkou = gyzks.Select(x => new WmsOptionDto { Id = x.Id, Name = x.ZhuangKouName }).ToList(); + + var deps = await _depRepo.GetAllAsync(); + result.Departments = deps.Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name }).ToList(); + + var gyss = await _gysRepo.GetAllAsync(); + result.Gongyingshangs = gyss.Select(x => new WmsOptionDto { Id = x.Id, Name = x.Name, Ext = x.Phone }).ToList(); + + return result; + } + + private async Task> WzDictAsync() + => (await _wzRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + private async Task> WzFullDictAsync() + => (await _wzRepo.GetAllAsync()).ToDictionary(x => x.Id, x => (x.Name, x.Code, x.Unit, x.Guige)); + + // ==================== 入库 ==================== + + public async Task> GetRukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize) + { + var all = await _rkRepo.GetAllAsync(); + var q = all.AsQueryable().Where(x => x.Module == (Wms_Module_Enum)module); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + if (type.HasValue) q = q.Where(x => x.Type == type.Value); + + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + var details = await _rkDetailRepo.GetAllAsync(); + var sumDict = details.Where(d => d.RukuId > 0) + .GroupBy(d => d.RukuId) + .ToDictionary(g => g.Key, g => new + { + Jianshu = g.Sum(x => x.Jianshu), + BaojianNum = g.Sum(x => x.BaojianNum), + Jingzhong = g.Sum(x => x.Jingzhong), + Shuliang = g.Sum(x => x.Shuliang), + Jine = g.Sum(x => x.Jine), + Count = g.Count() + }); + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.Code.Contains(key) || x.Kuguan.Contains(key) || x.Description.Contains(key) + || x.Caigouyuan.Contains(key) + || (x.GysId > 0 && gysDict.ContainsKey(x.GysId) && gysDict[x.GysId].Contains(key))); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sumDict.TryGetValue(x.Id, out var s); + return new WmsRukuListDto + { + Id = x.Id, Module = module, Date = x.Date, Type = x.Type, TypeName = RkTypeName(module, x.Type), + Code = x.Code, GysId = x.GysId, GysName = gysDict.GetValueOrDefault(x.GysId, ""), + KhId = x.KhId, KhName = gysDict.GetValueOrDefault(x.KhId, ""), + GyzkId = x.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(x.GyzkId, ""), + DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + Lianxiren = x.Lianxiren, Phone = x.Phone, Carno = x.Carno, Kuguan = x.Kuguan, + Caigouyuan = x.Caigouyuan, PayStatus = x.PayStatus, + RkStatus = x.RkStatus, RkStatusName = RkStatusName(x.RkStatus), + BxStatus = x.BxStatus, BxStatusName = BxStatusName(x.BxStatus), + Description = x.Description, + Jianshu = s?.Jianshu ?? 0, BaojianNum = s?.BaojianNum ?? 0, Jingzhong = s?.Jingzhong ?? 0, + Shuliang = s?.Shuliang ?? 0, Jine = s?.Jine ?? 0, DetailCount = s?.Count ?? 0 + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetRukuAsync(long id) + { + var ruku = await _rkRepo.GetByIdAsync(id); + if (ruku == null) throw new ServiceException("入库单不存在", 404); + var module = (int)ruku.Module; + + var details = await _rkDetailRepo.FindAsync(x => x.RukuId == id); + var bmds = await _bmdRepo.GetAllAsync(); + var bmdDict = bmds.Where(b => !b.IsChuku).GroupBy(b => b.DetailId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.Id).ToList()); + + var wzDict = await WzFullDictAsync(); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + return new WmsRukuEditDto + { + Id = ruku.Id, Module = module, Date = ruku.Date, Type = ruku.Type, Code = ruku.Code, + GysId = ruku.GysId, GysName = gysDict.GetValueOrDefault(ruku.GysId, ""), + KhId = ruku.KhId, KhName = gysDict.GetValueOrDefault(ruku.KhId, ""), + GyzkId = ruku.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(ruku.GyzkId, ""), + DepId = ruku.DepId, DepName = depDict.GetValueOrDefault(ruku.DepId, ""), + Lianxiren = ruku.Lianxiren, Phone = ruku.Phone, Carno = ruku.Carno, Kuguan = ruku.Kuguan, + Caigouyuan = ruku.Caigouyuan, PayStatus = ruku.PayStatus, + RkStatus = ruku.RkStatus, BxStatus = ruku.BxStatus, Description = ruku.Description, + Details = details.OrderBy(x => x.Id).Select(d => + { + wzDict.TryGetValue(d.WzmlId, out var wz); + return new WmsRukuDetailDto + { + Id = d.Id, RukuId = d.RukuId, WzmlId = d.WzmlId, + WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + YlzkId = d.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(d.YlzkId, ""), + GyzkId = d.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(d.GyzkId, ""), + DepId = d.DepId, DepName = depDict.GetValueOrDefault(d.DepId, ""), + Jianshu = d.Jianshu, Maozhong = d.Maozhong, Pizhong = d.Pizhong, Jingzhong = d.Jingzhong, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, Jine = d.Jine, Sunhao = d.Sunhao, + PiciCodes = d.PiciCodes, PiciNum = d.PiciNum, BaojianNum = d.BaojianNum, + PiciIds = d.PiciIds, + Banci = d.Banci, JidiWaigou = d.JidiWaigou, + Bangmadans = bmdDict.TryGetValue(d.Id, out var list) + ? list.Select(b => new WmsBangmadanDto + { + Id = b.Id, DetailId = b.DetailId, IsChuku = b.IsChuku, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, GuobangShijian = b.GuobangShijian + }).ToList() + : new List() + }; + }).ToList() + }; + } + + public async Task SaveRukuAsync(WmsRukuSaveDto dto) + { + var module = dto.Module; + if (module < 1 || module > 5) throw new ServiceException("模块无效"); + if (dto.Date == default) throw new ServiceException("请选择入库日期"); + if (dto.Details == null || dto.Details.Count == 0) throw new ServiceException("请至少录入一条入库明细"); + + WmsRuku entity; + var isNew = dto.Id == 0; + if (isNew) + { + entity = new WmsRuku { Module = (Wms_Module_Enum)module }; + } + else + { + entity = await _rkRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("入库单不存在", 404); + // 物资模块:先回滚库存,再按新明细重算 + if (module == (int)Wms_Module_Enum.物资) await RollbackWjKucunAsync(dto.Id, true); + } + + entity.Date = dto.Date.Date; + entity.Type = dto.Type; + entity.Code = (dto.Code ?? "").Trim(); + entity.GysId = dto.GysId; + entity.KhId = dto.KhId; + entity.GyzkId = dto.GyzkId; + entity.DepId = dto.DepId; + entity.Lianxiren = dto.Lianxiren ?? ""; + entity.Phone = dto.Phone ?? ""; + entity.Carno = dto.Carno ?? ""; + entity.Kuguan = dto.Kuguan ?? ""; + entity.Caigouyuan = dto.Caigouyuan ?? ""; + entity.PayStatus = dto.PayStatus ?? ""; + entity.RkStatus = dto.RkStatus; + entity.BxStatus = dto.BxStatus; + entity.Description = dto.Description ?? ""; + + // 单据编号唯一(同模块 + 同日期) + if (!string.IsNullOrWhiteSpace(entity.Code)) + { + var dup = await _rkRepo.FirstOrDefaultAsync(x => x.Id != entity.Id && x.Module == (Wms_Module_Enum)module + && x.Date == entity.Date && x.Code == entity.Code); + if (dup != null) throw new ServiceException($"单据编号「{entity.Code}」在该日期已存在"); + } + + if (isNew) await _rkRepo.AddAsync(entity); else await _rkRepo.UpdateAsync(entity); + + // 明细先删后建(物理删除,避免软删残留影响汇总) + var olds = await _rkDetailRepo.FindAsync(x => x.RukuId == entity.Id); + if (olds.Count > 0) + { + // 生丝:编辑前回滚旧批次的入库状态 + if (module == (int)Wms_Module_Enum.生丝) + await RollbackPiciRukuAsync(olds); + var oldBmds = await _bmdRepo.GetAllAsync(); + var del = oldBmds.Where(b => !b.IsChuku && olds.Any(o => o.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + await _rkDetailRepo.HardDeleteRangeAsync(olds); + } + + foreach (var d in dto.Details) + { + if (d.WzmlId == 0 && d.YlzkId == 0 && d.GyzkId == 0) throw new ServiceException("入库明细必须选择物资 / 庄口"); + + // 磅码单汇总:件数 / 毛重 / 皮重 / 净重 + var bmds = d.Bangmadans ?? new List(); + foreach (var b in bmds) b.Jingzhong = Round2(b.Maozhong - b.Pizhong); + + var detail = new WmsRukuDetail + { + RukuId = entity.Id, + WzmlId = d.WzmlId, YlzkId = d.YlzkId, GyzkId = d.GyzkId, DepId = d.DepId, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, + Sunhao = d.Sunhao, PiciCodes = d.PiciCodes ?? "", PiciNum = d.PiciNum, BaojianNum = d.BaojianNum, + PiciIds = d.PiciIds ?? "", + Banci = d.Banci ?? "", JidiWaigou = d.JidiWaigou ?? "" + }; + + if (bmds.Count > 0) + { + detail.Jianshu = bmds.Sum(b => b.Jianshu); + detail.Maozhong = Round2(bmds.Sum(b => b.Maozhong)); + detail.Pizhong = Round2(bmds.Sum(b => b.Pizhong)); + detail.Jingzhong = Round2(bmds.Sum(b => b.Jingzhong)); + } + else + { + detail.Jianshu = d.Jianshu; + detail.Maozhong = d.Maozhong; + detail.Pizhong = d.Pizhong; + detail.Jingzhong = d.Jingzhong; + } + + // 净重 = 毛重 − 皮重 − 包装重量 − 运损(各模块未使用的项为 0) + if (detail.Jingzhong == 0) + detail.Jingzhong = Round2(detail.Maozhong - detail.Pizhong - detail.BaozhuangZhongliang - detail.Sunhao); + + // 物资:金额 = 数量 × 单价(冲账允许负数) + if (module == (int)Wms_Module_Enum.物资) + { + if (detail.Sunhao == 0 && Math.Abs(d.Jine) > 0) + detail.Jine = d.Jine; + else + detail.Jine = Round2(detail.Shuliang * detail.Danjia); + if (detail.Shuliang != 0 && detail.Jine != 0 && detail.Danjia == 0) + detail.Danjia = Round4(detail.Jine / detail.Shuliang); + } + else + { + detail.Jine = d.Jine; + } + + await _rkDetailRepo.AddAsync(detail); + + // 生丝入库:按批次ID更新批次入库状态(对齐 F9 SSRk_CreateCommand.UpdateRk) + if (module == (int)Wms_Module_Enum.生丝 && !string.IsNullOrWhiteSpace(detail.PiciIds)) + await UpdatePiciRukuAsync(detail.PiciIds, detail.Id, entity.Date); + + foreach (var b in bmds) + { + await _bmdRepo.AddAsync(new WmsBangmadan + { + DetailId = detail.Id, IsChuku = false, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + GuobangShijian = b.GuobangShijian + }); + } + + // 物资模块:入库即增库存 + if (module == (int)Wms_Module_Enum.物资) + await ApplyWjKucunAsync(detail.WzmlId, detail.Shuliang, detail.Jine); + } + + return entity.Id; + } + + public async Task DeleteRukuAsync(long id) + { + var ruku = await _rkRepo.GetByIdAsync(id); + if (ruku == null) throw new ServiceException("入库单不存在", 404); + + if (ruku.Module == Wms_Module_Enum.物资) await RollbackWjKucunAsync(id, true); + + var details = await _rkDetailRepo.FindAsync(x => x.RukuId == id); + if (ruku.Module == Wms_Module_Enum.生丝) await RollbackPiciRukuAsync(details); + var bmds = await _bmdRepo.GetAllAsync(); + var del = bmds.Where(b => !b.IsChuku && details.Any(d => d.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + if (details.Count > 0) await _rkDetailRepo.HardDeleteRangeAsync(details); + // 物理删除:单据编号有唯一索引,软删残留会阻止重新创建同号单据 + await _rkRepo.HardDeleteAsync(ruku); + } + + /// 采购报销:把入库单置为「已报销」(物资模块) + public async Task BaoxiaoAsync(List rukuIds) + { + if (rukuIds == null || rukuIds.Count == 0) throw new ServiceException("请选择要报销的入库单"); + var count = 0; + foreach (var id in rukuIds) + { + var ruku = await _rkRepo.GetByIdAsync(id); + if (ruku == null) continue; + ruku.BxStatus = 2; + await _rkRepo.UpdateAsync(ruku); + count++; + } + return count; + } + + // ==================== 出库 ==================== + + public async Task> GetChukuPagedAsync(int module, DateTime? start, DateTime? end, int? type, string like, int pageIndex, int pageSize) + { + var all = await _ckRepo.GetAllAsync(); + var q = all.AsQueryable().Where(x => x.Module == (Wms_Module_Enum)module); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + if (type.HasValue) q = q.Where(x => x.Type == type.Value); + + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var wzDict = await WzDictAsync(); + + var details = await _ckDetailRepo.GetAllAsync(); + var sumDict = details.GroupBy(d => d.ChukuId) + .ToDictionary(g => g.Key, g => new + { + Jianshu = g.Sum(x => x.Jianshu), + Jingzhong = g.Sum(x => x.Jingzhong), + Shuliang = g.Sum(x => x.Shuliang), + Jine = g.Sum(x => x.Jine), + Count = g.Count() + }); + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.Code.Contains(key) || x.Kuguan.Contains(key) || x.Lingliaoren.Contains(key) + || x.Description.Contains(key) + || (x.WldwId > 0 && gysDict.ContainsKey(x.WldwId) && gysDict[x.WldwId].Contains(key))); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => + { + sumDict.TryGetValue(x.Id, out var s); + return new WmsChukuListDto + { + Id = x.Id, Module = module, Date = x.Date, Type = x.Type, TypeName = CkTypeName(module, x.Type), + Code = x.Code, + WldwId = x.WldwId, WldwName = gysDict.GetValueOrDefault(x.WldwId, ""), + KhId = x.KhId, KhName = gysDict.GetValueOrDefault(x.KhId, ""), + GyzkId = x.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(x.GyzkId, ""), + LingliaoDepId = x.LingliaoDepId, LingliaoDepName = depDict.GetValueOrDefault(x.LingliaoDepId, ""), + UseDepId = x.UseDepId, UseDepName = depDict.GetValueOrDefault(x.UseDepId, ""), + DbdepId = x.DbdepId, DbdepName = depDict.GetValueOrDefault(x.DbdepId, ""), + Lingliaoren = x.Lingliaoren, Kuguan = x.Kuguan, UseLocation = x.UseLocation, + CkStatus = x.CkStatus, CkStatusName = CkStatusName(x.CkStatus), IsHongchong = x.IsHongchong, + Description = x.Description, + Jianshu = s?.Jianshu ?? 0, Jingzhong = s?.Jingzhong ?? 0, + Shuliang = s?.Shuliang ?? 0, Jine = s?.Jine ?? 0, DetailCount = s?.Count ?? 0 + }; + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetChukuAsync(long id) + { + var chuku = await _ckRepo.GetByIdAsync(id); + if (chuku == null) throw new ServiceException("出库单不存在", 404); + + var details = await _ckDetailRepo.FindAsync(x => x.ChukuId == id); + var bmds = await _bmdRepo.GetAllAsync(); + var bmdDict = bmds.Where(b => b.IsChuku).GroupBy(b => b.DetailId) + .ToDictionary(g => g.Key, g => g.OrderBy(x => x.Id).ToList()); + + var wzDict = await WzFullDictAsync(); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + return new WmsChukuEditDto + { + Id = chuku.Id, Module = (int)chuku.Module, Date = chuku.Date, Type = chuku.Type, Code = chuku.Code, + WldwId = chuku.WldwId, WldwName = gysDict.GetValueOrDefault(chuku.WldwId, ""), + KhId = chuku.KhId, KhName = gysDict.GetValueOrDefault(chuku.KhId, ""), + GyzkId = chuku.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(chuku.GyzkId, ""), + LingliaoDepId = chuku.LingliaoDepId, LingliaoDepName = depDict.GetValueOrDefault(chuku.LingliaoDepId, ""), + UseDepId = chuku.UseDepId, UseDepName = depDict.GetValueOrDefault(chuku.UseDepId, ""), + DbdepId = chuku.DbdepId, DbdepName = depDict.GetValueOrDefault(chuku.DbdepId, ""), + Lingliaoren = chuku.Lingliaoren, Kuguan = chuku.Kuguan, UseLocation = chuku.UseLocation, + CkStatus = chuku.CkStatus, IsHongchong = chuku.IsHongchong, HongchongId = chuku.HongchongId, + Description = chuku.Description, + Details = details.OrderBy(x => x.Id).Select(d => + { + wzDict.TryGetValue(d.WzmlId, out var wz); + return new WmsChukuDetailDto + { + Id = d.Id, ChukuId = d.ChukuId, WzmlId = d.WzmlId, + WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + YlzkId = d.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(d.YlzkId, ""), + GyzkId = d.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(d.GyzkId, ""), + DepId = d.DepId, DepName = depDict.GetValueOrDefault(d.DepId, ""), + Jianshu = d.Jianshu, Maozhong = d.Maozhong, Pizhong = d.Pizhong, Jingzhong = d.Jingzhong, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, Jine = d.Jine, KucunNum = d.KucunNum, + PiciCodes = d.PiciCodes, PiciNum = d.PiciNum, PcAndBjIds = d.PcAndBjIds, + Lingyongren = d.Lingyongren, Banci = d.Banci, + JidiWaigou = d.JidiWaigou, + Bangmadans = bmdDict.TryGetValue(d.Id, out var list) + ? list.Select(b => new WmsBangmadanDto + { + Id = b.Id, DetailId = b.DetailId, IsChuku = b.IsChuku, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, GuobangShijian = b.GuobangShijian + }).ToList() + : new List() + }; + }).ToList() + }; + } + + public async Task SaveChukuAsync(WmsChukuSaveDto dto) + { + var module = dto.Module; + if (module < 1 || module > 5) throw new ServiceException("模块无效"); + if (dto.Date == default) throw new ServiceException("请选择出库日期"); + if (dto.Details == null || dto.Details.Count == 0) throw new ServiceException("请至少录入一条出库明细"); + + WmsChuku entity; + var isNew = dto.Id == 0; + if (isNew) + { + entity = new WmsChuku { Module = (Wms_Module_Enum)module }; + } + else + { + entity = await _ckRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("出库单不存在", 404); + if (module == (int)Wms_Module_Enum.物资) await RollbackWjKucunAsync(dto.Id, false); + } + + entity.Date = dto.Date.Date; + entity.Type = dto.Type; + entity.Code = (dto.Code ?? "").Trim(); + entity.WldwId = dto.WldwId; + entity.KhId = dto.KhId; + entity.GyzkId = dto.GyzkId; + entity.LingliaoDepId = dto.LingliaoDepId; + entity.UseDepId = dto.UseDepId; + entity.DbdepId = dto.DbdepId; + entity.Lingliaoren = dto.Lingliaoren ?? ""; + entity.Kuguan = dto.Kuguan ?? ""; + entity.UseLocation = dto.UseLocation ?? ""; + entity.CkStatus = dto.CkStatus; + entity.CkTime = dto.CkStatus == 1 ? DateTime.Now : null; + entity.Description = dto.Description ?? ""; + + if (!string.IsNullOrWhiteSpace(entity.Code)) + { + var dup = await _ckRepo.FirstOrDefaultAsync(x => x.Id != entity.Id && x.Module == (Wms_Module_Enum)module + && x.Date == entity.Date && x.Code == entity.Code); + if (dup != null) throw new ServiceException($"单据编号「{entity.Code}」在该日期已存在"); + } + + if (isNew) await _ckRepo.AddAsync(entity); else await _ckRepo.UpdateAsync(entity); + + var olds = await _ckDetailRepo.FindAsync(x => x.ChukuId == entity.Id); + if (olds.Count > 0) + { + // 生丝:编辑前回滚旧批次的包件出库状态 + if (module == (int)Wms_Module_Enum.生丝) + await RollbackPiciChukuAsync(olds); + var oldBmds = await _bmdRepo.GetAllAsync(); + var del = oldBmds.Where(b => b.IsChuku && olds.Any(o => o.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + await _ckDetailRepo.HardDeleteRangeAsync(olds); + } + + var kucunDict = (await _kucunRepo.GetAllAsync()).ToDictionary(x => x.WzmlId); + + foreach (var d in dto.Details) + { + if (d.WzmlId == 0 && d.YlzkId == 0 && d.GyzkId == 0) throw new ServiceException("出库明细必须选择物资 / 庄口"); + + var bmds = d.Bangmadans ?? new List(); + foreach (var b in bmds) b.Jingzhong = Round2(b.Maozhong - b.Pizhong); + + var detail = new WmsChukuDetail + { + ChukuId = entity.Id, + WzmlId = d.WzmlId, YlzkId = d.YlzkId, GyzkId = d.GyzkId, DepId = d.DepId, + BiaobaoZhongliang = d.BiaobaoZhongliang, BaozhuangZhongliang = d.BaozhuangZhongliang, + Shuliang = d.Shuliang, Danjia = d.Danjia, KucunNum = d.KucunNum, + PiciCodes = d.PiciCodes ?? "", PiciNum = d.PiciNum, PcAndBjIds = d.PcAndBjIds ?? "", + Lingyongren = d.Lingyongren ?? "", + Banci = d.Banci ?? "", JidiWaigou = d.JidiWaigou ?? "" + }; + + if (bmds.Count > 0) + { + detail.Jianshu = bmds.Sum(b => b.Jianshu); + detail.Maozhong = Round2(bmds.Sum(b => b.Maozhong)); + detail.Pizhong = Round2(bmds.Sum(b => b.Pizhong)); + detail.Jingzhong = Round2(bmds.Sum(b => b.Jingzhong)); + } + else + { + detail.Jianshu = d.Jianshu; + detail.Maozhong = d.Maozhong; + detail.Pizhong = d.Pizhong; + detail.Jingzhong = d.Jingzhong; + } + if (detail.Jingzhong == 0) + detail.Jingzhong = Round2(detail.Maozhong - detail.Pizhong - detail.BaozhuangZhongliang); + + if (module == (int)Wms_Module_Enum.物资) + { + if (detail.Shuliang <= 0) throw new ServiceException("出库数量必须大于0"); + + // 库存校验 + 金额口径(F9 WJ_Chuku_Detail.CalJine) + kucunDict.TryGetValue(detail.WzmlId, out var kc); + var kcNum = kc?.Shuliang ?? d.KucunNum; + var kcJine = kc?.Jine ?? 0; + var kcDanjia = kc?.Danjia ?? 0; + detail.KucunNum = kcNum; + if (detail.Shuliang > kcNum) throw new ServiceException("库存数量不足"); + + if (detail.Shuliang == kcNum) + { + detail.Jine = kcJine; + detail.Danjia = detail.Shuliang != 0 ? Round4(detail.Jine / detail.Shuliang) : 0; + } + else + { + detail.Danjia = kcDanjia; + detail.Jine = Round2(detail.Shuliang * detail.Danjia); + } + + await ApplyWjKucunAsync(detail.WzmlId, -detail.Shuliang, -detail.Jine); + } + else + { + detail.Jine = d.Jine; + } + + await _ckDetailRepo.AddAsync(detail); + + foreach (var b in bmds) + { + await _bmdRepo.AddAsync(new WmsBangmadan + { + DetailId = detail.Id, IsChuku = true, Jianshu = b.Jianshu, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + GuobangShijian = b.GuobangShijian + }); + } + + // 生丝出库:按批次+包件更新(对齐 F9 SSCk_CreateCommand.Update_Ck) + if (module == (int)Wms_Module_Enum.生丝 && !string.IsNullOrWhiteSpace(detail.PcAndBjIds)) + await UpdatePiciChukuAsync(detail.PcAndBjIds, detail.Id, entity.Date); + } + + return entity.Id; + } + + public async Task DeleteChukuAsync(long id) + { + var chuku = await _ckRepo.GetByIdAsync(id); + if (chuku == null) throw new ServiceException("出库单不存在", 404); + + if (chuku.Module == Wms_Module_Enum.物资) await RollbackWjKucunAsync(id, false); + + var details = await _ckDetailRepo.FindAsync(x => x.ChukuId == id); + if (chuku.Module == Wms_Module_Enum.生丝) await RollbackPiciChukuAsync(details); + var bmds = await _bmdRepo.GetAllAsync(); + var del = bmds.Where(b => b.IsChuku && details.Any(d => d.Id == b.DetailId)).ToList(); + if (del.Count > 0) await _bmdRepo.HardDeleteRangeAsync(del); + if (details.Count > 0) await _ckDetailRepo.HardDeleteRangeAsync(details); + await _ckRepo.HardDeleteAsync(chuku); + } + + // ==================== 库存台账 ==================== + + public async Task> GetTzDatesAsync(int module, int kind) + { + var dates = await _tzDateRepo.GetAllAsync(); + var items = await _tzItemRepo.GetAllAsync(); + var countDict = items.GroupBy(x => x.TzDateId).ToDictionary(g => g.Key, g => g.Count()); + + return dates.Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == kind) + .OrderByDescending(x => x.Date) + .Select(x => new WmsTzDateDto + { + Id = x.Id, Module = module, Kind = kind, Date = x.Date, LastDate = x.LastDate, + ItemCount = countDict.GetValueOrDefault(x.Id, 0) + }).ToList(); + } + + /// 读取(或生成)指定日期台账 + public async Task GetTaizhangAsync(int module, int kind, DateTime date, bool regenerate) + { + var tzDate = date.Date; + var existing = await _tzDateRepo.FirstOrDefaultAsync(x => x.Module == (Wms_Module_Enum)module + && x.Kind == kind && x.Date == tzDate); + + if (existing == null) + { + var latest = (await _tzDateRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == kind) + .OrderByDescending(x => x.Date).FirstOrDefault(); + if (latest != null && tzDate <= latest.Date) + throw new ServiceException($"只能生成晚于 {latest.Date:yyyy-MM-dd} 的台账"); + + // F9:无上一台账时 LastDate 取 1970-01-01,即首份台账汇总全部历史出入库 + var lastDate = latest?.Date ?? new DateTime(1970, 1, 1); + var generated = await GenerateItemsAsync(module, kind, tzDate, lastDate); + return new WmsTzEditDto + { + TzDateId = 0, Module = module, Kind = kind, Date = tzDate, LastDate = lastDate, Items = generated + }; + } + + if (regenerate) + { + var generated = await GenerateItemsAsync(module, kind, tzDate, existing.LastDate); + return new WmsTzEditDto + { + TzDateId = existing.Id, Module = module, Kind = kind, Date = tzDate, LastDate = existing.LastDate, Items = generated + }; + } + + var items = await _tzItemRepo.FindAsync(x => x.TzDateId == existing.Id); + return new WmsTzEditDto + { + TzDateId = existing.Id, Module = module, Kind = kind, Date = existing.Date, LastDate = existing.LastDate, + Items = items.OrderBy(x => x.Id).Select(ToTzItemDto).ToList() + }; + } + + /// + /// 生成台账:上日结存(上一台账中「件数或净重不为 0」的行)+ 区间入库/出库汇总 → 计算今日结存。 + /// 对齐 F9 YL_Kucun_Taizhang_Edit.GenerateTaizhang / CalTdayJiecun。 + /// + private async Task> GenerateItemsAsync(int module, int kind, DateTime tzDate, DateTime lastDate) + { + var from = kind == 1 ? lastDate : lastDate.AddDays(1); // 笼筒架台账含上日(F9 区间 Yesterday~Date) + var to = tzDate.AddDays(1).AddMilliseconds(-1); + + var result = new List(); + + var lastItems = await _tzItemRepo.GetAllAsync(); + var lastDict = lastItems.Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == kind && x.Date == lastDate) + .ToList(); + foreach (var last in lastDict) + { + if (last.Tdayjingzhong != 0 || last.Tdayjianshu != 0) + { + result.Add(new WmsTzItemDto + { + ItemId = last.ItemId, SubKey = last.SubKey, ItemName = last.ItemName, ItemCode = last.ItemCode, + Ydayjianshu = last.Tdayjianshu, Ydayjingzhong = last.Tdayjingzhong, + Yangjianjianshu = last.Yangjianjianshu + }); + } + } + + var rukuSum = await SumRukuAsync(module, kind, from, to); + var chukuSum = await SumChukuAsync(module, kind, from, to); + + void Merge(List rows, bool isRuku) + { + foreach (var r in rows) + { + var hit = result.FirstOrDefault(x => x.ItemId == r.ItemId && x.SubKey == r.SubKey); + if (hit == null) + { + hit = new WmsTzItemDto { ItemId = r.ItemId, SubKey = r.SubKey }; + result.Add(hit); + } + if (isRuku) { hit.Tdayrukujianshu = r.Jianshu; hit.Tdayrukujingzhong = r.Jingzhong; } + else { hit.Tdaychukujianshu = r.Jianshu; hit.Tdaychukujingzhong = r.Jingzhong; } + } + } + Merge(rukuSum, true); + Merge(chukuSum, false); + + var nameDict = await BuildItemNameDictAsync(module, kind, result.Select(x => x.ItemId).Distinct().ToList()); + foreach (var item in result) + { + if (nameDict.TryGetValue(item.ItemId, out var nm)) { item.ItemName = nm.Name; item.ItemCode = nm.Code; } + CalTdayJiecun(item, module); + } + + return result.OrderBy(x => x.ItemName).ToList(); + } + + /// 今日结存 = 上日 + 入库 − 出库 + 盈亏;全部出库 = 上日有结存且今日结存为 0(对齐 F9 CalTdayJiecun) + private static void CalTdayJiecun(WmsTzItemDto item, int module) + { + item.Tdayjianshu = item.Ydayjianshu + item.Tdayrukujianshu - item.Tdaychukujianshu; + item.Tdayjingzhong = Round2(item.Ydayjingzhong + item.Tdayrukujingzhong - item.Tdaychukujingzhong + item.Yingkui); + if (module != (int)Wms_Module_Enum.物资) + item.AllChuku = item.Ydayjingzhong != 0 && item.Tdayjingzhong == 0; + } + + public async Task SaveTaizhangAsync(WmsTzSaveDto dto) + { + var module = dto.Module; + var tzDate = dto.Date.Date; + + if (dto.Items == null || dto.Items.Count == 0) throw new ServiceException("台账明细不能为空"); + + // 校验(对齐 F9 YL_Kucun_Taizhang_Edit 校验规则) + foreach (var it in dto.Items) + { + var expect = Round2(it.Ydayjingzhong + it.Tdayrukujingzhong - it.Tdaychukujingzhong + it.Yingkui); + if (expect != Round2(it.Tdayjingzhong)) + throw new ServiceException($"「{it.ItemName}」今日结存重量有误(应为 {expect})"); + if (it.Tdayjingzhong < 0) + throw new ServiceException($"「{it.ItemName}」结存重量不能小于0"); + if (module == (int)Wms_Module_Enum.原料 && it.AllChuku && it.Yangjianjianshu != 0) + throw new ServiceException($"「{it.ItemName}」全部出库时样茧件数必须为0"); + } + + WmsKcTzDate tz; + if (dto.TzDateId > 0) + { + tz = await _tzDateRepo.GetByIdAsync(dto.TzDateId); + if (tz == null) throw new ServiceException("台账不存在", 404); + } + else + { + var exist = await _tzDateRepo.FirstOrDefaultAsync(x => x.Module == (Wms_Module_Enum)module + && x.Kind == dto.Kind && x.Date == tzDate); + if (exist != null) throw new ServiceException("该日期台账已存在"); + + var latest = (await _tzDateRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == dto.Kind) + .OrderByDescending(x => x.Date).FirstOrDefault(); + if (latest != null && tzDate <= latest.Date) + throw new ServiceException($"只能生成晚于 {latest.Date:yyyy-MM-dd} 的台账"); + + tz = new WmsKcTzDate + { + Module = (Wms_Module_Enum)module, Kind = dto.Kind, Date = tzDate, + LastDate = latest?.Date ?? new DateTime(1970, 1, 1) + }; + await _tzDateRepo.AddAsync(tz); + } + + var olds = await _tzItemRepo.FindAsync(x => x.TzDateId == tz.Id); + if (olds.Count > 0) await _tzItemRepo.HardDeleteRangeAsync(olds); + + foreach (var it in dto.Items) + { + await _tzItemRepo.AddAsync(new WmsKcTzItem + { + TzDateId = tz.Id, Module = (Wms_Module_Enum)module, Kind = dto.Kind, Date = tzDate, + ItemId = it.ItemId, SubKey = it.SubKey ?? "", ItemName = it.ItemName ?? "", ItemCode = it.ItemCode ?? "", + Ydayjianshu = it.Ydayjianshu, Ydayjingzhong = it.Ydayjingzhong, + Tdayrukujianshu = it.Tdayrukujianshu, Tdayrukujingzhong = it.Tdayrukujingzhong, + Tdaychukujianshu = it.Tdaychukujianshu, Tdaychukujingzhong = it.Tdaychukujingzhong, + Yingkui = it.Yingkui, Tdayjianshu = it.Tdayjianshu, Tdayjingzhong = it.Tdayjingzhong, + Yangjianjianshu = it.Yangjianjianshu, AllChuku = it.AllChuku, Description = it.Description ?? "" + }); + } + + return tz.Id; + } + + public async Task DeleteTaizhangAsync(long id) + { + var tz = await _tzDateRepo.GetByIdAsync(id); + if (tz == null) throw new ServiceException("台账不存在", 404); + + var items = await _tzItemRepo.FindAsync(x => x.TzDateId == id); + if (items.Count > 0) await _tzItemRepo.HardDeleteRangeAsync(items); + await _tzDateRepo.HardDeleteAsync(tz); + } + + // ==================== 汇总辅助 ==================== + + private class SumRow + { + public long ItemId { get; set; } + public string SubKey { get; set; } = ""; + public decimal Jianshu { get; set; } + public decimal Jingzhong { get; set; } + } + + private long ItemKeyOf(WmsRukuDetail d, int module, int kind) + => IsYlzkItem(module, kind) ? d.YlzkId : (IsGyzkItem(module, kind) ? d.GyzkId : d.WzmlId); + + private string SubKeyOf(WmsRukuDetail d, int module) + => module == (int)Wms_Module_Enum.生丝 ? (d.JidiWaigou ?? "") : ""; + + private long ItemKeyOf(WmsChukuDetail d, int module, int kind) + => IsYlzkItem(module, kind) ? d.YlzkId : (IsGyzkItem(module, kind) ? d.GyzkId : d.WzmlId); + + private string SubKeyOf(WmsChukuDetail d, int module) + => module == (int)Wms_Module_Enum.生丝 ? (d.JidiWaigou ?? "") : ""; + + private async Task> SumRukuAsync(int module, int kind, DateTime from, DateTime to) + { + var rukus = (await _rkRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date >= from && x.Date <= to).ToList(); + if (rukus.Count == 0) return new List(); + var ids = rukus.Select(x => x.Id).ToHashSet(); + var details = (await _rkDetailRepo.GetAllAsync()).Where(x => ids.Contains(x.RukuId)).ToList(); + + if (IsGyzkItem(module, kind)) + { + var ltjWzmlId = await GetLtjWzmlIdAsync(); + if (ltjWzmlId > 0) details = details.Where(x => x.WzmlId == ltjWzmlId).ToList(); + } + + return details.GroupBy(d => new { Key = ItemKeyOf(d, module, kind), Sub = SubKeyOf(d, module) }) + .Select(g => new SumRow + { + ItemId = g.Key.Key, + SubKey = g.Key.Sub, + Jianshu = module == (int)Wms_Module_Enum.物资 ? g.Sum(x => x.Shuliang) + : module == (int)Wms_Module_Enum.生丝 ? g.Sum(x => x.PiciNum) + : g.Sum(x => x.Jianshu), + Jingzhong = module == (int)Wms_Module_Enum.物资 ? Round2(g.Sum(x => x.Jine)) : Round2(g.Sum(x => x.Jingzhong)) + }) + .Where(x => x.ItemId != 0) + .ToList(); + } + + private async Task> SumChukuAsync(int module, int kind, DateTime from, DateTime to) + { + var chukus = (await _ckRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date >= from && x.Date <= to).ToList(); + if (chukus.Count == 0) return new List(); + var ids = chukus.Select(x => x.Id).ToHashSet(); + var details = (await _ckDetailRepo.GetAllAsync()).Where(x => ids.Contains(x.ChukuId)).ToList(); + + if (IsGyzkItem(module, kind)) + { + var ltjWzmlId = await GetLtjWzmlIdAsync(); + if (ltjWzmlId > 0) details = details.Where(x => x.WzmlId == ltjWzmlId).ToList(); + } + + return details.GroupBy(d => new { Key = ItemKeyOf(d, module, kind), Sub = SubKeyOf(d, module) }) + .Select(g => new SumRow + { + ItemId = g.Key.Key, + SubKey = g.Key.Sub, + Jianshu = module == (int)Wms_Module_Enum.物资 ? g.Sum(x => x.Shuliang) + : module == (int)Wms_Module_Enum.生丝 ? g.Sum(x => x.PiciNum) + : g.Sum(x => x.Jianshu), + Jingzhong = module == (int)Wms_Module_Enum.物资 ? Round2(g.Sum(x => x.Jine)) : Round2(g.Sum(x => x.Jingzhong)) + }) + .Where(x => x.ItemId != 0) + .ToList(); + } + + /// 笼筒架台账只统计「联统茧」物资(注册表「仓储\联统茧物资ID」) + private async Task GetLtjWzmlIdAsync() + { + var wzs = await _wzRepo.GetAllAsync(); + var hit = wzs.FirstOrDefault(x => x.Name.Contains("联统")); + return hit?.Id ?? 0; + } + + private async Task> BuildItemNameDictAsync(int module, int kind, List ids) + { + var dict = new Dictionary(); + if (IsYlzkItem(module, kind)) + { + foreach (var x in await _ylzkRepo.GetAllAsync()) dict[x.Id] = (x.Name, x.Code); + } + else if (IsGyzkItem(module, kind)) + { + foreach (var x in await _gyzkRepo.GetAllAsync()) dict[x.Id] = (x.ZhuangKouName, ""); + } + else + { + foreach (var x in await _wzRepo.GetAllAsync()) dict[x.Id] = (x.Name, x.Code); + } + return dict; + } + + private static WmsTzItemDto ToTzItemDto(WmsKcTzItem x) => new() + { + Id = x.Id, TzDateId = x.TzDateId, ItemId = x.ItemId, SubKey = x.SubKey, + ItemName = x.ItemName, ItemCode = x.ItemCode, + Ydayjianshu = x.Ydayjianshu, Ydayjingzhong = x.Ydayjingzhong, + Tdayrukujianshu = x.Tdayrukujianshu, Tdayrukujingzhong = x.Tdayrukujingzhong, + Tdaychukujianshu = x.Tdaychukujianshu, Tdaychukujingzhong = x.Tdaychukujingzhong, + Yingkui = x.Yingkui, Tdayjianshu = x.Tdayjianshu, Tdayjingzhong = x.Tdayjingzhong, + Yangjianjianshu = x.Yangjianjianshu, AllChuku = x.AllChuku, Description = x.Description + }; + + // ==================== 物资库存 / 出入库流水 ==================== + + /// 物资模块库存:按物资目录汇总(入库 − 出库) + public async Task> GetKucunAsync(int module, string like) + { + var wzFull = await WzFullDictAsync(); + var result = new List(); + + if (module == (int)Wms_Module_Enum.物资) + { + var kcs = await _kucunRepo.GetAllAsync(); + foreach (var kc in kcs) + { + wzFull.TryGetValue(kc.WzmlId, out var wz); + result.Add(new WmsKucunDto + { + WzmlId = kc.WzmlId, WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", + Unit = wz.Unit ?? "", Guige = wz.Guige ?? "", + Shuliang = kc.Shuliang, Danjia = kc.Danjia, Jine = kc.Jine + }); + } + } + else + { + // 其余模块:用最近一次台账的「今日结存」作为库存 + var dates = (await _tzDateRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Kind == 0) + .OrderByDescending(x => x.Date).FirstOrDefault(); + if (dates != null) + { + var items = await _tzItemRepo.FindAsync(x => x.TzDateId == dates.Id); + foreach (var it in items.Where(x => x.Tdayjianshu != 0 || x.Tdayjingzhong != 0)) + { + wzFull.TryGetValue(it.ItemId, out var wz); + result.Add(new WmsKucunDto + { + WzmlId = it.ItemId, WzmlName = it.ItemName, WzmlCode = it.ItemCode, + Unit = wz.Unit ?? "", Guige = wz.Guige ?? "", + Shuliang = it.Tdayjianshu, Jine = it.Tdayjingzhong + }); + } + } + } + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + result = result.Where(x => x.WzmlName.Contains(key) || x.WzmlCode.Contains(key)).ToList(); + } + return result.OrderBy(x => x.WzmlCode).ToList(); + } + + /// 出入库流水(物资模块) + public async Task> GetCrkAsync(int module, DateTime start, DateTime end, long? wzmlId) + { + var wzFull = await WzFullDictAsync(); + var list = new List(); + + var rukus = (await _rkRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date.Date >= start.Date && x.Date.Date <= end.Date).ToList(); + var rkIds = rukus.Select(x => x.Id).ToHashSet(); + var rkDetails = (await _rkDetailRepo.GetAllAsync()).Where(x => rkIds.Contains(x.RukuId)).ToList(); + var rkDict = rukus.ToDictionary(x => x.Id); + + foreach (var d in rkDetails) + { + if (wzmlId.HasValue && d.WzmlId != wzmlId.Value) continue; + wzFull.TryGetValue(d.WzmlId, out var wz); + rkDict.TryGetValue(d.RukuId, out var rk); + list.Add(new WmsCrkDto + { + Date = rk?.Date ?? default, IsRuku = true, Code = rk?.Code ?? "", + TypeName = RkTypeName(module, rk?.Type ?? 0), + WzmlId = d.WzmlId, WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + Shuliang = d.Shuliang, Danjia = d.Danjia, Jine = d.Jine, Description = "" + }); + } + + var chukus = (await _ckRepo.GetAllAsync()) + .Where(x => x.Module == (Wms_Module_Enum)module && x.Date.Date >= start.Date && x.Date.Date <= end.Date).ToList(); + var ckIds = chukus.Select(x => x.Id).ToHashSet(); + var ckDetails = (await _ckDetailRepo.GetAllAsync()).Where(x => ckIds.Contains(x.ChukuId)).ToList(); + var ckDict = chukus.ToDictionary(x => x.Id); + + foreach (var d in ckDetails) + { + if (wzmlId.HasValue && d.WzmlId != wzmlId.Value) continue; + wzFull.TryGetValue(d.WzmlId, out var wz); + ckDict.TryGetValue(d.ChukuId, out var ck); + list.Add(new WmsCrkDto + { + Date = ck?.Date ?? default, IsRuku = false, Code = ck?.Code ?? "", + TypeName = CkTypeName(module, ck?.Type ?? 0), + WzmlId = d.WzmlId, WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + Shuliang = -d.Shuliang, Danjia = d.Danjia, Jine = -d.Jine, Description = "" + }); + } + + return list.OrderByDescending(x => x.Date).ThenBy(x => x.Code).ToList(); + } + + /// 物资库存增减(入库为正、出库为负) + private async Task ApplyWjKucunAsync(long wzmlId, decimal shuliang, decimal jine) + { + if (wzmlId == 0) return; + var kc = await _kucunRepo.FirstOrDefaultAsync(x => x.WzmlId == wzmlId); + if (kc == null) + { + kc = new WmsKucun { WzmlId = wzmlId }; + await _kucunRepo.AddAsync(kc); + } + // 出库前已做库存校验,正常业务不会出现负数;删除单据回滚时可能出现负数, + // 这里不做截断,保证「入库回滚」与「出库回滚」严格对称,不会造成库存漂移。 + kc.Shuliang = Round4(kc.Shuliang + shuliang); + kc.Jine = Round2(kc.Jine + jine); + kc.Danjia = kc.Shuliang != 0 ? Round4(kc.Jine / kc.Shuliang) : 0; + await _kucunRepo.UpdateAsync(kc); + } + + /// 回滚单据对物资库存的影响(修改 / 删除前调用) + private async Task RollbackWjKucunAsync(long billId, bool isRuku) + { + if (isRuku) + { + var details = await _rkDetailRepo.FindAsync(x => x.RukuId == billId); + foreach (var d in details) await ApplyWjKucunAsync(d.WzmlId, -d.Shuliang, -d.Jine); + } + else + { + var details = await _ckDetailRepo.FindAsync(x => x.ChukuId == billId); + foreach (var d in details) await ApplyWjKucunAsync(d.WzmlId, d.Shuliang, d.Jine); + } + } + + // ==================== 生丝批次 / 包件 ==================== + + public async Task> GetPiciPagedAsync(DateTime? start, DateTime? end, int kcStatus, string like, int pageIndex, int pageSize) + { + var all = await _piciRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + if (kcStatus > 0) q = q.Where(x => x.KcStatus == kcStatus); + + var wzDict = await WzDictAsync(); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var baojianDict = (await _baojianRepo.GetAllAsync()).GroupBy(x => x.PiciId) + .ToDictionary(g => g.Key, g => g.Count()); + + if (!string.IsNullOrWhiteSpace(like)) + { + var key = like.Trim(); + q = q.Where(x => x.Code.Contains(key) || x.Guige.Contains(key)); + } + + var total = q.LongCount(); + var items = q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code) + .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList(); + + return new PageResult + { + Items = items.Select(x => new WmsPiciListDto + { + Id = x.Id, Code = x.Code, CodeType = x.CodeType, Year = x.Year, Date = x.Date, ScDate = x.ScDate, + GyzkId = x.GyzkId, GyzkName = gyzkDict.GetValueOrDefault(x.GyzkId, ""), + WzmlId = x.WzmlId, WzmlName = wzDict.GetValueOrDefault(x.WzmlId, ""), + DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + Guige = x.Guige, JidiWaigou = x.JidiWaigou, YoujiPutong = x.YoujiPutong, + Baoshu = x.Baoshu, StartBaojianNum = x.StartBaojianNum, + Maozhong = x.Maozhong, Pizhong = x.Pizhong, Jingzhong = x.Jingzhong, Gongliang = x.Gongliang, + KcStatus = x.KcStatus, KcStatusName = KcStatusName(x.KcStatus), + RkItemId = x.RkItemId, RkDate = x.RkDate, + CkLastDate = x.CkLastDate, CkBaoshu = x.CkBaoshu, CkJingzhong = x.CkJingzhong, KcBaoshu = x.KcBaoshu, + Dengji = x.Dengji, Qingjie = x.Qingjie, Jiejing = x.Jiejing, + Piancha = x.Piancha, Zongcha = x.Zongcha, Qieduan = x.Qieduan, + Description = x.Description, BaojianNum = baojianDict.GetValueOrDefault(x.Id, 0) + }).ToList(), + Total = total, PageIndex = pageIndex, PageSize = pageSize + }; + } + + public async Task GetPiciAsync(long id) + { + var pici = await _piciRepo.GetByIdAsync(id); + if (pici == null) throw new ServiceException("批次不存在", 404); + var baojians = await _baojianRepo.FindAsync(x => x.PiciId == id); + + return new WmsPiciSaveDto + { + Id = pici.Id, Code = pici.Code, CodeType = pici.CodeType, Date = pici.Date, ScDate = pici.ScDate, + GyzkId = pici.GyzkId, WzmlId = pici.WzmlId, DepId = pici.DepId, Guige = pici.Guige, + JidiWaigou = pici.JidiWaigou, YoujiPutong = pici.YoujiPutong, + Baoshu = pici.Baoshu, StartBaojianNum = pici.StartBaojianNum, + Maozhong = pici.Maozhong, Pizhong = pici.Pizhong, Jingzhong = pici.Jingzhong, Gongliang = pici.Gongliang, + KcStatus = pici.KcStatus, Dengji = pici.Dengji, Qingjie = pici.Qingjie, Jiejing = pici.Jiejing, + Piancha = pici.Piancha, Zongcha = pici.Zongcha, Qieduan = pici.Qieduan, Description = pici.Description, + Baojians = baojians.OrderBy(x => x.Baojianhao).Select(b => new WmsPiciBaojianDto + { + Id = b.Id, PiciId = b.PiciId, Baojianhao = b.Baojianhao, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + Gongliang = b.Gongliang, Huichaolv = b.Huichaolv, + CkIsck = b.CkIsck, CkItemId = b.CkItemId, CkDate = b.CkDate, CkJingzhong = b.CkJingzhong, + Description = b.Description + }).ToList() + }; + } + + public async Task SavePiciAsync(WmsPiciSaveDto dto) + { + if (string.IsNullOrWhiteSpace(dto.Code)) throw new ServiceException("请填写批号"); + + // 批号格式校验(对齐 F9 Page_PiciBaojian_Edit):正品 10 位数字;其他 31EE-7 位数字 + var codeOk = dto.CodeType == 0 + ? System.Text.RegularExpressions.Regex.IsMatch(dto.Code.Trim(), @"^\d{10}$") + : System.Text.RegularExpressions.Regex.IsMatch(dto.Code.Trim(), @"^31EE-\d{7}$"); + if (!codeOk) + throw new ServiceException("编号格式错误:正品编号类型 示例6100000001;其他编号类型 示例31EE-2600001"); + + WmsPici entity; + var isNew = dto.Id == 0; + if (isNew) + { + var exist = await _piciRepo.FirstOrDefaultAsync(x => x.Code == dto.Code); + if (exist != null) throw new ServiceException("批号已存在"); + entity = new WmsPici(); + } + else + { + entity = await _piciRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("批次不存在", 404); + } + + entity.Code = dto.Code.Trim(); + entity.CodeType = dto.CodeType; + entity.Date = dto.Date.Date; + entity.Year = dto.Date.Year; + entity.ScDate = dto.ScDate; + entity.GyzkId = dto.GyzkId; + entity.WzmlId = dto.WzmlId; + entity.DepId = dto.DepId; + entity.Guige = dto.Guige ?? ""; + entity.JidiWaigou = dto.JidiWaigou ?? ""; + entity.YoujiPutong = dto.YoujiPutong ?? ""; + entity.Baoshu = dto.Baoshu; + entity.StartBaojianNum = dto.StartBaojianNum; + entity.Dengji = dto.Dengji ?? ""; + entity.Qingjie = dto.Qingjie; + entity.Jiejing = dto.Jiejing; + entity.Piancha = dto.Piancha; + entity.Zongcha = dto.Zongcha; + entity.Qieduan = dto.Qieduan; + entity.Description = dto.Description ?? ""; + + // 包件:净重 = 毛重 − 皮重;公量 = 净重 × (1 + 11%) / (1 + 回潮率%) + var baojians = dto.Baojians ?? new List(); + foreach (var b in baojians) + { + b.Jingzhong = Round2(b.Maozhong - b.Pizhong); + b.Gongliang = Round2(b.Jingzhong * 1.11m / (1 + b.Huichaolv / 100m)); + } + + if (baojians.Count > 0) + { + entity.Maozhong = Round2(baojians.Sum(b => b.Maozhong)); + entity.Pizhong = Round2(baojians.Sum(b => b.Pizhong)); + entity.Jingzhong = Round2(baojians.Sum(b => b.Jingzhong)); + entity.Gongliang = Round2(baojians.Sum(b => b.Gongliang)); + entity.Baoshu = baojians.Count; + } + else + { + entity.Maozhong = dto.Maozhong; + entity.Pizhong = dto.Pizhong; + entity.Jingzhong = dto.Jingzhong; + entity.Gongliang = dto.Gongliang; + } + + if (entity.KcStatus == 0) entity.KcStatus = 1; // 待入库 + + if (isNew) await _piciRepo.AddAsync(entity); else await _piciRepo.UpdateAsync(entity); + + var olds = await _baojianRepo.FindAsync(x => x.PiciId == entity.Id); + if (olds.Count > 0) await _baojianRepo.HardDeleteRangeAsync(olds); + foreach (var b in baojians) + { + await _baojianRepo.AddAsync(new WmsPiciBaojian + { + PiciId = entity.Id, Baojianhao = b.Baojianhao, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + Gongliang = b.Gongliang, Huichaolv = b.Huichaolv, Description = b.Description ?? "" + }); + } + + return entity.Id; + } + + public async Task DeletePiciAsync(long id) + { + var pici = await _piciRepo.GetByIdAsync(id); + if (pici == null) throw new ServiceException("批次不存在", 404); + var baojians = await _baojianRepo.FindAsync(x => x.PiciId == id); + if (baojians.Count > 0) await _baojianRepo.HardDeleteRangeAsync(baojians); + // 物理删除:批号有唯一索引,软删残留会阻止重新创建同号批次 + await _piciRepo.HardDeleteAsync(pici); + } + + // ==================== 生丝批次:入库 / 出库状态流转(对齐 F9 SSRk/SSCk CQRS) ==================== + + private class PcAndBjRef { public long PiciId { get; set; } public List PiciBaojianIds { get; set; } = new(); } + + /// 入库:按批次ID把批次置为「已入库」(对齐 F9 SSRk_CreateCommand.UpdateRk) + private async Task UpdatePiciRukuAsync(string piciIds, long rkItemId, DateTime rkDate) + { + foreach (var pid in SplitLongs(piciIds)) + { + var pici = await _piciRepo.GetByIdAsync(pid); + if (pici == null) continue; + pici.RkItemId = rkItemId; + pici.RkDate = rkDate; + pici.KcStatus = 2; // 已入库 + pici.KcBaoshu = pici.Baoshu; + pici.CkBaoshu = 0; + pici.CkJingzhong = 0; + await _piciRepo.UpdateAsync(pici); + } + } + + /// 入库回滚:把已入库且未出库的批次退回「待入库」(编辑/删除入库单时调用) + private async Task RollbackPiciRukuAsync(List details) + { + var ids = details.Where(d => !string.IsNullOrWhiteSpace(d.PiciIds)) + .SelectMany(d => SplitLongs(d.PiciIds)).Distinct().ToList(); + foreach (var pid in ids) + { + var pici = await _piciRepo.GetByIdAsync(pid); + if (pici == null) continue; + if (pici.CkBaoshu > 0 || pici.KcStatus == 3 || pici.KcStatus == 4) + throw new ServiceException($"批次「{pici.Code}」已出库,不能修改/删除入库单"); + pici.RkItemId = 0; + pici.RkDate = null; + pici.KcStatus = 1; // 待入库 + pici.KcBaoshu = 0; + await _piciRepo.UpdateAsync(pici); + } + } + + /// 出库:按「批次+包件」标记包件已出库并更新批次库存状态(对齐 F9 SSCk_CreateCommand.Update_Ck) + private async Task UpdatePiciChukuAsync(string pcAndBjIds, long ckItemId, DateTime ckDate) + { + List refs; + try + { + refs = System.Text.Json.JsonSerializer.Deserialize>(pcAndBjIds, + new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new(); + } + catch { refs = new(); } + + var ckDate0 = ckDate.Date; + foreach (var r in refs) + { + if (r.PiciBaojianIds == null || r.PiciBaojianIds.Count == 0) continue; + var pici = await _piciRepo.GetByIdAsync(r.PiciId); + if (pici == null) continue; + + // 校验(对齐 F9):待入库不可出库;已全部出库不可再出库 + if (pici.KcStatus == 1) throw new ServiceException($"批次「{pici.Code}」尚未入库,不能出库"); + if (pici.KcStatus == 4) throw new ServiceException($"批次「{pici.Code}」已全部出库"); + + var baojians = await _baojianRepo.FindAsync(x => x.PiciId == r.PiciId); + var selected = baojians.Where(x => r.PiciBaojianIds.Contains(x.Id)).ToList(); + if (selected.Count == 0) continue; + + foreach (var bj in selected) + { + if (bj.CkIsck) continue; + bj.CkIsck = true; + bj.CkItemId = ckItemId; + bj.CkDate = ckDate0; + bj.CkJingzhong = bj.Jingzhong; + await _baojianRepo.UpdateAsync(bj); + } + + var ckBaoshu = selected.Count; + var ckJingzhong = Round2(selected.Sum(x => x.Jingzhong)); + + await _piciChukuRepo.AddAsync(new WmsPiciChuku + { + PiciId = pici.Id, CkItemId = ckItemId, CkBaoshu = ckBaoshu, + CkDate = ckDate0, CkJingzhong = ckJingzhong + }); + + pici.CkItemId = ckItemId; + pici.CkLastDate = ckDate0; + pici.CkBaoshu += ckBaoshu; + pici.CkJingzhong = Round2(pici.CkJingzhong + ckJingzhong); + pici.KcBaoshu = pici.Baoshu - pici.CkBaoshu; + pici.KcStatus = pici.CkBaoshu >= pici.Baoshu ? 4 : 3; // 全部出库 / 部分出库 + await _piciRepo.UpdateAsync(pici); + } + } + + /// 出库回滚:撤销包件出库标记并回退批次库存状态(编辑/删除出库单时调用) + private async Task RollbackPiciChukuAsync(List details) + { + var itemIds = details.Select(d => d.Id).ToList(); + if (itemIds.Count == 0) return; + + var bjs = await _baojianRepo.GetAllAsync(); + var hitBjs = bjs.Where(x => itemIds.Contains(x.CkItemId)).ToList(); + foreach (var bj in hitBjs) + { + bj.CkIsck = false; + bj.CkItemId = 0; + bj.CkDate = null; + bj.CkJingzhong = 0; + await _baojianRepo.UpdateAsync(bj); + } + + var chukus = await _piciChukuRepo.GetAllAsync(); + var hitCks = chukus.Where(x => itemIds.Contains(x.CkItemId)).ToList(); + if (hitCks.Count > 0) await _piciChukuRepo.HardDeleteRangeAsync(hitCks); + + // 按批次重算库存状态 + var piciIds = hitCks.Select(x => x.PiciId).Distinct().ToList(); + foreach (var pid in piciIds) + { + var pici = await _piciRepo.GetByIdAsync(pid); + if (pici == null) continue; + var remain = chukus.Where(x => x.PiciId == pid && !hitCks.Contains(x)).ToList(); + pici.CkBaoshu = remain.Sum(x => x.CkBaoshu); + pici.CkJingzhong = Round2(remain.Sum(x => x.CkJingzhong)); + pici.KcBaoshu = pici.Baoshu - pici.CkBaoshu; + pici.CkItemId = remain.Count > 0 ? remain.Max(x => x.CkItemId) : 0; + pici.CkLastDate = remain.Count > 0 ? (remain.Max(x => x.CkDate) ?? default) : default; + pici.KcStatus = pici.CkBaoshu == 0 ? 2 : (pici.CkBaoshu >= pici.Baoshu ? 4 : 3); + await _piciRepo.UpdateAsync(pici); + } + } + + private static List SplitLongs(string s) + => (s ?? "").Split(new[] { ',', ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Select(x => { long.TryParse(x.Trim(), out var v); return v; }) + .Where(v => v > 0).Distinct().ToList(); + + /// 生丝出库选择批次+包件(对齐 F9 SSPici_GetChukuSelectAsync) + public async Task> GetPiciChukuSelectAsync(long wzmlId, string excludePiciIds) + { + var all = await _piciRepo.GetAllAsync(); + var q = all.Where(x => x.KcStatus == 2 || x.KcStatus == 3); // 已入库 / 部分出库 + if (wzmlId > 0) q = q.Where(x => x.WzmlId == wzmlId); + var excl = SplitLongs(excludePiciIds); + if (excl.Count > 0) q = q.Where(x => !excl.Contains(x.Id)); + + var wzDict = await WzDictAsync(); + var gyzkDict = (await _gyzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.ZhuangKouName); + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var baojians = await _baojianRepo.GetAllAsync(); + + return q.OrderByDescending(x => x.Date).ThenBy(x => x.Code).Select(p => + { + var bjs = baojians.Where(b => b.PiciId == p.Id).OrderBy(b => b.Baojianhao).ToList(); + var kcBjs = bjs.Where(b => !b.CkIsck).ToList(); + return new WmsPiciChukuSelectDto + { + PiciId = p.Id, PiciCode = p.Code, Date = p.Date, Guige = p.Guige, + WzmlName = wzDict.GetValueOrDefault(p.WzmlId, ""), + GyzkName = gyzkDict.GetValueOrDefault(p.GyzkId, ""), + DepName = depDict.GetValueOrDefault(p.DepId, ""), + JidiWaigou = p.JidiWaigou, + Baoshu = p.Baoshu, KcBaoshu = kcBjs.Count, + Jingzhong = p.Jingzhong, KcJingzhong = Round2(kcBjs.Sum(x => x.Jingzhong)), + Description = p.Description, + Baojians = bjs.Select(b => new WmsPiciBaojianDto + { + Id = b.Id, PiciId = b.PiciId, Baojianhao = b.Baojianhao, + Maozhong = b.Maozhong, Pizhong = b.Pizhong, Jingzhong = b.Jingzhong, + Gongliang = b.Gongliang, Huichaolv = b.Huichaolv, + CkIsck = b.CkIsck, CkItemId = b.CkItemId, CkDate = b.CkDate, CkJingzhong = b.CkJingzhong, + Description = b.Description + }).ToList() + }; + }).ToList(); + } + + /// 读取批次质检 + public async Task GetPiciQcAsync(long piciId) + { + var qc = await _piciQcRepo.FirstOrDefaultAsync(x => x.PiciId == piciId); + return qc == null ? new WmsPiciQcDto { PiciId = piciId } : ToQcDto(qc); + } + + public async Task SavePiciQcAsync(WmsPiciQcDto dto) + { + if (dto.PiciId <= 0) throw new ServiceException("批次无效"); + var qc = await _piciQcRepo.FirstOrDefaultAsync(x => x.PiciId == dto.PiciId); + var isNew = qc == null; + if (isNew) qc = new WmsPiciQc { PiciId = dto.PiciId }; + + qc.Pingwei = dto.Pingwei ?? ""; qc.Seze = dto.Seze ?? ""; + qc.AvgHuichaolv = dto.AvgHuichaolv; qc.AvgGongliangXiandu = dto.AvgGongliangXiandu; + qc.XianduPiancha = dto.XianduPiancha; qc.XianduZdpiancha = dto.XianduZdpiancha; + qc.Qingjie = dto.Qingjie; qc.Jiejing = dto.Jiejing; + qc.JunyunYd = dto.JunyunYd; qc.JunyunEd = dto.JunyunEd; qc.JunyunSd = dto.JunyunSd; + qc.Qieduan = dto.Qieduan; + qc.DuanlieQiangdu = dto.DuanlieQiangdu ?? ""; qc.DuanlieShenchanglv = dto.DuanlieShenchanglv ?? ""; + qc.Baohe = dto.Baohe; + qc.WgWg = dto.WgWg ?? ""; qc.WgYanse = dto.WgYanse ?? ""; qc.WgGuangze = dto.WgGuangze ?? ""; qc.WgShougan = dto.WgShougan ?? ""; + qc.QjZhuyao = dto.QjZhuyao; qc.QjCiyaoFeisi = dto.QjCiyaoFeisi; qc.QjCiyaoDacao = dto.QjCiyaoDacao; + qc.QjCiyaoNianfucao = dto.QjCiyaoNianfucao; qc.QjCiyaoDachangjie = dto.QjCiyaoDachangjie; + qc.QjCiyaoZhongluoxuan = dto.QjCiyaoZhongluoxuan; + qc.QjPutongXiaocao = dto.QjPutongXiaocao; qc.QjPutongChangjie = dto.QjPutongChangjie; + qc.QjPutongLuoxuan = dto.QjPutongLuoxuan; qc.QjPutongHuanjie = dto.QjPutongHuanjie; + qc.Jj100 = dto.Jj100; qc.Jj95 = dto.Jj95; qc.Jj90 = dto.Jj90; qc.Jj85 = dto.Jj85; qc.Jj80 = dto.Jj80; + qc.Jj75 = dto.Jj75; qc.Jj70 = dto.Jj70; qc.Jj65 = dto.Jj65; qc.Jj60 = dto.Jj60; qc.Jj55 = dto.Jj55; + qc.Jj50 = dto.Jj50; qc.Jj45 = dto.Jj45; + qc.JjYuanyin = dto.JjYuanyin ?? ""; qc.Description = dto.Description ?? ""; + + if (isNew) await _piciQcRepo.AddAsync(qc); else await _piciQcRepo.UpdateAsync(qc); + } + + private static WmsPiciQcDto ToQcDto(WmsPiciQc qc) => new() + { + Id = qc.Id, PiciId = qc.PiciId, Pingwei = qc.Pingwei, Seze = qc.Seze, + AvgHuichaolv = qc.AvgHuichaolv, AvgGongliangXiandu = qc.AvgGongliangXiandu, + XianduPiancha = qc.XianduPiancha, XianduZdpiancha = qc.XianduZdpiancha, + Qingjie = qc.Qingjie, Jiejing = qc.Jiejing, + JunyunYd = qc.JunyunYd, JunyunEd = qc.JunyunEd, JunyunSd = qc.JunyunSd, Qieduan = qc.Qieduan, + DuanlieQiangdu = qc.DuanlieQiangdu, DuanlieShenchanglv = qc.DuanlieShenchanglv, Baohe = qc.Baohe, + WgWg = qc.WgWg, WgYanse = qc.WgYanse, WgGuangze = qc.WgGuangze, WgShougan = qc.WgShougan, + QjZhuyao = qc.QjZhuyao, QjCiyaoFeisi = qc.QjCiyaoFeisi, QjCiyaoDacao = qc.QjCiyaoDacao, + QjCiyaoNianfucao = qc.QjCiyaoNianfucao, QjCiyaoDachangjie = qc.QjCiyaoDachangjie, + QjCiyaoZhongluoxuan = qc.QjCiyaoZhongluoxuan, + QjPutongXiaocao = qc.QjPutongXiaocao, QjPutongChangjie = qc.QjPutongChangjie, + QjPutongLuoxuan = qc.QjPutongLuoxuan, QjPutongHuanjie = qc.QjPutongHuanjie, + Jj100 = qc.Jj100, Jj95 = qc.Jj95, Jj90 = qc.Jj90, Jj85 = qc.Jj85, Jj80 = qc.Jj80, + Jj75 = qc.Jj75, Jj70 = qc.Jj70, Jj65 = qc.Jj65, Jj60 = qc.Jj60, Jj55 = qc.Jj55, + Jj50 = qc.Jj50, Jj45 = qc.Jj45, JjYuanyin = qc.JjYuanyin, Description = qc.Description + }; + + /// 读取批次纤度检验 + public async Task> GetPiciQcXdjyAsync(long piciId) + { + var list = await _piciQcXdjyRepo.FindAsync(x => x.PiciId == piciId); + return list.OrderBy(x => x.Id).Select(x => new WmsPiciQcXdjyDto + { Id = x.Id, PiciId = x.PiciId, Xiandu = x.Xiandu, Num = x.Num }).ToList(); + } + + public async Task SavePiciQcXdjyAsync(List items) + { + var piciId = items?.FirstOrDefault()?.PiciId ?? 0; + if (piciId <= 0) throw new ServiceException("批次无效"); + var olds = await _piciQcXdjyRepo.FindAsync(x => x.PiciId == piciId); + if (olds.Count > 0) await _piciQcXdjyRepo.HardDeleteRangeAsync(olds); + foreach (var it in items ?? new List()) + { + await _piciQcXdjyRepo.AddAsync(new WmsPiciQcXdjy + { PiciId = piciId, Xiandu = it.Xiandu, Num = it.Num }); + } + } + + // ==================== 原料采购 ==================== + + public async Task> GetCaigouAsync(DateTime? start, DateTime? end) + { + var all = await _caigouRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var gysDict = (await _gysRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + + return q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code).Select(x => new WmsCaigouDto + { + Id = x.Id, Date = x.Date, Code = x.Code, + GysId = x.GysId, GysName = gysDict.GetValueOrDefault(x.GysId, ""), + YlzkId = x.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(x.YlzkId, ""), + Jianshu = x.Jianshu, Maozhong = x.Maozhong, Pizhong = x.Pizhong, Jingzhong = x.Jingzhong, + Danjia = x.Danjia, Jine = x.Jine, Description = x.Description + }).ToList(); + } + + public async Task SaveCaigouAsync(WmsCaigouDto dto) + { + WmsCaigou entity; + var isNew = dto.Id == 0; + if (isNew) entity = new WmsCaigou(); + else + { + entity = await _caigouRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("采购记录不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.Code = dto.Code ?? ""; + entity.GysId = dto.GysId; + entity.YlzkId = dto.YlzkId; + entity.Jianshu = dto.Jianshu; + entity.Maozhong = dto.Maozhong; + entity.Pizhong = dto.Pizhong; + entity.Jingzhong = dto.Jingzhong != 0 ? dto.Jingzhong : Round2(dto.Maozhong - dto.Pizhong); + entity.Danjia = dto.Danjia; + entity.Jine = dto.Jine != 0 ? dto.Jine : Round2(entity.Jingzhong * dto.Danjia); + entity.Description = dto.Description ?? ""; + + if (isNew) await _caigouRepo.AddAsync(entity); else await _caigouRepo.UpdateAsync(entity); + return entity.Id; + } + + public async Task DeleteCaigouAsync(long id) + { + var entity = await _caigouRepo.GetByIdAsync(id); + if (entity == null) throw new ServiceException("采购记录不存在", 404); + await _caigouRepo.DeleteAsync(entity); + } + + // ==================== 原料翻包计划 ==================== + + public async Task> GetFbplanAsync(DateTime? start, DateTime? end) + { + var all = await _fbplanRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var ylzkDict = (await _ylzkRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var wzDict = await WzDictAsync(); + var jls = await _fbjlRepo.GetAllAsync(); + var jlDict = jls.GroupBy(x => x.FbplanId).ToDictionary(g => g.Key, g => g.OrderBy(x => x.Date).ToList()); + + return q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code).ToList().Select(x => + { + jlDict.TryGetValue(x.Id, out var records); + records ??= new List(); + return new WmsFbplanDto + { + Id = x.Id, Date = x.Date, Code = x.Code, + YlzkId = x.YlzkId, YlzkName = ylzkDict.GetValueOrDefault(x.YlzkId, ""), + WzmlId = x.WzmlId, WzmlName = wzDict.GetValueOrDefault(x.WzmlId, ""), + Jianshu = x.Jianshu, Jingzhong = x.Jingzhong, + FbType = x.FbType, FbTypeName = FbTypeName(x.FbType), + RukuStatus = x.RukuStatus, RukuStatusName = RukuStatusName(x.RukuStatus), + Description = x.Description, + YiwanchengJianshu = records.Sum(r => r.Jianshu), + YiwanchengJingzhong = records.Sum(r => r.Jingzhong), + Records = records.Select(r => new WmsFbjlDto + { + Id = r.Id, FbplanId = r.FbplanId, Date = r.Date, + Jianshu = r.Jianshu, Jingzhong = r.Jingzhong, Description = r.Description + }).ToList() + }; + }).ToList(); + } + + public async Task SaveFbplanAsync(WmsFbplanDto dto) + { + WmsFbplan entity; + var isNew = dto.Id == 0; + if (isNew) entity = new WmsFbplan(); + else + { + entity = await _fbplanRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("翻包计划不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.Code = dto.Code ?? ""; + entity.YlzkId = dto.YlzkId; + entity.WzmlId = dto.WzmlId; + entity.Jianshu = dto.Jianshu; + entity.Jingzhong = dto.Jingzhong; + entity.FbType = dto.FbType == 0 ? 1 : dto.FbType; + entity.RukuStatus = dto.RukuStatus == 0 ? 1 : dto.RukuStatus; + entity.Description = dto.Description ?? ""; + + if (isNew) await _fbplanRepo.AddAsync(entity); else await _fbplanRepo.UpdateAsync(entity); + + // 翻包记录整体覆盖 + var olds = await _fbjlRepo.FindAsync(x => x.FbplanId == entity.Id); + if (olds.Count > 0) await _fbjlRepo.HardDeleteRangeAsync(olds); + foreach (var r in dto.Records ?? new List()) + { + await _fbjlRepo.AddAsync(new WmsFbjl + { + FbplanId = entity.Id, Date = r.Date.Date, Jianshu = r.Jianshu, Jingzhong = r.Jingzhong, + Description = r.Description ?? "" + }); + } + + return entity.Id; + } + + public async Task DeleteFbplanAsync(long id) + { + var entity = await _fbplanRepo.GetByIdAsync(id); + if (entity == null) throw new ServiceException("翻包计划不存在", 404); + var jls = await _fbjlRepo.FindAsync(x => x.FbplanId == id); + if (jls.Count > 0) await _fbjlRepo.HardDeleteRangeAsync(jls); + await _fbplanRepo.DeleteAsync(entity); + } + + // ==================== 物资采购计划 ==================== + + public async Task> GetCgplanAsync(DateTime? start, DateTime? end) + { + var all = await _cgplanRepo.GetAllAsync(); + var q = all.AsQueryable(); + if (start.HasValue) q = q.Where(x => x.Date.Date >= start.Value.Date); + if (end.HasValue) q = q.Where(x => x.Date.Date <= end.Value.Date); + + var depDict = (await _depRepo.GetAllAsync()).ToDictionary(x => x.Id, x => x.Name); + var items = await _cgplanItemRepo.GetAllAsync(); + var itemDict = items.GroupBy(x => x.CgplanId).ToDictionary(g => g.Key, g => g.ToList()); + + return q.OrderByDescending(x => x.Date).ThenByDescending(x => x.Code).ToList().Select(x => + { + itemDict.TryGetValue(x.Id, out var list); + list ??= new List(); + return new WmsCgplanListDto + { + Id = x.Id, Date = x.Date, Code = x.Code, + DepId = x.DepId, DepName = depDict.GetValueOrDefault(x.DepId, ""), + Status = x.Status, StatusName = CgplanStatusName(x.Status), + Description = x.Description, + ItemCount = list.Count, + Shuliang = list.Sum(i => i.Shuliang), + Jine = list.Sum(i => i.Jine) + }; + }).ToList(); + } + + public async Task GetCgplanAsync(long id) + { + var plan = await _cgplanRepo.GetByIdAsync(id); + if (plan == null) throw new ServiceException("采购计划不存在", 404); + var items = await _cgplanItemRepo.FindAsync(x => x.CgplanId == id); + var wzFull = await WzFullDictAsync(); + + return new WmsCgplanSaveDto + { + Id = plan.Id, Date = plan.Date, Code = plan.Code, DepId = plan.DepId, Status = plan.Status, + Description = plan.Description, + Items = items.OrderBy(x => x.Id).Select(i => + { + wzFull.TryGetValue(i.WzmlId, out var wz); + return new WmsCgplanItemDto + { + Id = i.Id, CgplanId = i.CgplanId, WzmlId = i.WzmlId, + WzmlName = wz.Name ?? "", WzmlCode = wz.Code ?? "", Unit = wz.Unit ?? "", + Shuliang = i.Shuliang, Danjia = i.Danjia, Jine = i.Jine, Description = i.Description + }; + }).ToList() + }; + } + + public async Task SaveCgplanAsync(WmsCgplanSaveDto dto) + { + WmsCgplan entity; + var isNew = dto.Id == 0; + if (isNew) entity = new WmsCgplan(); + else + { + entity = await _cgplanRepo.GetByIdAsync(dto.Id); + if (entity == null) throw new ServiceException("采购计划不存在", 404); + } + + entity.Date = dto.Date.Date; + entity.Code = dto.Code ?? ""; + entity.DepId = dto.DepId; + entity.Status = dto.Status; + entity.Description = dto.Description ?? ""; + + if (isNew) await _cgplanRepo.AddAsync(entity); else await _cgplanRepo.UpdateAsync(entity); + + var olds = await _cgplanItemRepo.FindAsync(x => x.CgplanId == entity.Id); + if (olds.Count > 0) await _cgplanItemRepo.HardDeleteRangeAsync(olds); + + foreach (var i in dto.Items ?? new List()) + { + if (i.WzmlId == 0) continue; + var jine = i.Jine != 0 ? i.Jine : Round2(i.Shuliang * i.Danjia); + await _cgplanItemRepo.AddAsync(new WmsCgplanItem + { + CgplanId = entity.Id, WzmlId = i.WzmlId, Shuliang = i.Shuliang, + Danjia = i.Danjia, Jine = jine, Description = i.Description ?? "" + }); + } + + return entity.Id; + } + + public async Task DeleteCgplanAsync(long id) + { + var entity = await _cgplanRepo.GetByIdAsync(id); + if (entity == null) throw new ServiceException("采购计划不存在", 404); + var items = await _cgplanItemRepo.FindAsync(x => x.CgplanId == id); + if (items.Count > 0) await _cgplanItemRepo.HardDeleteRangeAsync(items); + await _cgplanRepo.DeleteAsync(entity); + } + + // ==================== 工具 ==================== + + private static decimal Round2(decimal v) => Math.Round(v, 2, MidpointRounding.AwayFromZero); + private static decimal Round4(decimal v) => Math.Round(v, 4, MidpointRounding.AwayFromZero); +} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs deleted file mode 100644 index b10c635..0000000 --- a/backend/F9WebAiTest.Domain/Entities/Attendance/AttendanceRecord.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FreeSql.DataAnnotations; -using F9WebAiTest.Domain.Common; - -namespace F9WebAiTest.Domain.Entities.Attendance; - -/// 考勤打卡记录 -[Table(Name = "AttendanceRecords")] -[Index("IX_AttendanceRecords_EmployeeId_WorkDate", "EmployeeId, WorkDate", IsUnique = true)] -public class AttendanceRecord : BaseEntity -{ - public long EmployeeId { get; set; } - public DateTime WorkDate { get; set; } // 考勤日期 - public DateTime? SignInTime { get; set; } // 签到时间 - public DateTime? SignOutTime { get; set; } // 签退时间 - public string SignInStatus { get; set; } = ""; // 正常/迟到/缺卡 - public string SignOutStatus { get; set; } = ""; // 正常/早退/缺卡 - public string Remark { get; set; } = ""; -} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs deleted file mode 100644 index 1accc77..0000000 --- a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveApprover.cs +++ /dev/null @@ -1,18 +0,0 @@ -using FreeSql.DataAnnotations; -using F9WebAiTest.Domain.Common; - -namespace F9WebAiTest.Domain.Entities.Attendance; - -/// 请假审批节点(多级审批流程,OrderNo 表示审批顺序) -[Table(Name = "LeaveApprovers")] -[Index("IX_LeaveApprovers_LeaveId_OrderNo", "LeaveId, OrderNo", IsUnique = true)] -public class LeaveApprover : BaseEntity -{ - public long LeaveId { get; set; } - public long ApproverId { get; set; } // 审批人ID - public string ApproverName { get; set; } = ""; // 审批人姓名(冗余) - public int OrderNo { get; set; } // 序号(从1开始,表示审批顺序) - public string Status { get; set; } = "待审批"; // 待审批/已批准/已驳回 - public string ApproveRemark { get; set; } = ""; - public DateTime? ApprovedAt { get; set; } -} diff --git a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs b/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs deleted file mode 100644 index f40f1a3..0000000 --- a/backend/F9WebAiTest.Domain/Entities/Attendance/LeaveRequest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using FreeSql.DataAnnotations; -using F9WebAiTest.Domain.Common; - -namespace F9WebAiTest.Domain.Entities.Attendance; - -/// 请假申请 -[Table(Name = "LeaveRequests")] -public class LeaveRequest : BaseEntity -{ - public long EmployeeId { get; set; } - public string Type { get; set; } = "事假"; // 事假/病假/年假/调休/婚假/产假 - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - public string Reason { get; set; } = ""; - public string Status { get; set; } = "待审批"; // 待审批/已批准/已驳回 - public int CurrentOrderNo { get; set; } = 1; // 当前审批到第几步(多级审批) - public long? ApproverId { get; set; } - public string ApproverName { get; set; } = ""; - public string ApproveRemark { get; set; } = ""; - public DateTime? ApprovedAt { get; set; } -} diff --git a/backend/F9WebAiTest.Domain/Entities/PayRoll/CalcLdcct.cs b/backend/F9WebAiTest.Domain/Entities/PayRoll/CalcLdcct.cs new file mode 100644 index 0000000..f8fb2c8 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/PayRoll/CalcLdcct.cs @@ -0,0 +1,55 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.PayRoll; + +/// +/// 薪资计算-立缫挡车车头(挡车工 / 车头工 产量计算明细) +/// 唯一键:日期 + 部门 + 班次 + 机组 + 职工 + 工种 + 工艺庄口 + 规格 + 车台 +/// +[Table(Name = "CalcLdccts")] +[Index("IX_CalcLdccts_Biz", "Date, DepId, Banci, Jizu, EmployeeId, WorktypeId, GyzkId, Guige, Chetai", IsUnique = true)] +public class CalcLdcct : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long EmployeeId { get; set; } // 职工 + public long DepId { get; set; } // 部门 + public long WorktypeId { get; set; } // 工种 + public int Jizu { get; set; } // 机组 + public int Banci { get; set; } // 班次(0=甲 1=乙,对应 Prod_Banci_Enum) + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + public string Chetai { get; set; } = ""; // 车台(车头工为空) + + public decimal HoursKantai { get; set; } // 看台时长 + public decimal Dangcheshu { get; set; } // 挡车数 + public string Dengji { get; set; } = ""; // 等级 + public decimal Dengjifen { get; set; } // 等级分 + + // ---- 产量 ---- + public decimal ClDanjiaBase { get; set; } // 基础单价 + public decimal ClDanjiaDengji { get; set; } // 等级单价增减 + public decimal ClDanjiaActual { get; set; } // 实际单价 + public decimal ClSiliang { get; set; } // 丝量(g) + + // ---- 质量 ---- + public decimal ZlSaozhe { get; set; } // 缫折 + public decimal ZlCenterxd { get; set; } // 中心纤度数 + public decimal ZlYexiannum { get; set; } // 野纤数 + public decimal ZlQingjie { get; set; } // 清洁扣款片数 + public decimal ZlJiejing { get; set; } // 洁净扣款片数 + public decimal ZlErdu { get; set; } // 二度 + public decimal ZlSandu { get; set; } // 三度 + + // ---- 金额 ---- + public decimal JineCl_Original { get; set; } // 金额-产量-初始值 + public decimal Xishu_JineCl { get; set; } // 产量金额系数 + public decimal JineCl { get; set; } // 金额-产量 + public decimal JineZlSaozhe { get; set; } // 缫折考核 + public decimal JineZlCenterxd { get; set; } // 中心纤度奖 + public decimal JineZlYexian { get; set; } // 野纤扣 + public decimal JineZlQingjie { get; set; } // 清洁扣 + public decimal JineZlJiejing { get; set; } // 洁净扣 + public decimal JineZlErdu { get; set; } // 二度扣 + public decimal JineZlSandu { get; set; } // 三度扣 +} diff --git a/backend/F9WebAiTest.Domain/Entities/PayRoll/Conf.cs b/backend/F9WebAiTest.Domain/Entities/PayRoll/Conf.cs new file mode 100644 index 0000000..85f2421 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/PayRoll/Conf.cs @@ -0,0 +1,118 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.PayRoll; + +// 薪资配置(对应 F9 WinClient\F9.Business.PayRoll\PAYROLL 下的 CONFGYZK / CONFGZ / CONFDCG / DCXDJK / XinZiJiaGePeiZhi) + +/// 薪资配置-工艺庄口(按 年度 + 月份 + 工艺庄口 唯一) +[Table(Name = "ConfGyzks")] +[Index("IX_ConfGyzks_Year_Month_GyzkId", "Year, Month, GyzkId", IsUnique = true)] +public class ConfGyzk : BaseEntity +{ + public int Year { get; set; } // 年度 + public int Month { get; set; } // 月份 + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + + public decimal QsTaichan { get; set; } // 台产(工艺台时产量,单价分母) + public string QsDengji { get; set; } = ""; // 等级 + public decimal QsDengjifen { get; set; } // 等级分 + public decimal SaoZhe { get; set; } // 工艺指标缫折 + public int MeiTongJianLiang { get; set; } // 每桶茧量 + public int LuoSiTongShu { get; set; } // 落丝桶数 + public decimal MeiCheSiLiang { get; set; } // 每车丝量(kg) + public decimal DcgJijianXishu { get; set; } // 挡车工计件系数 + public decimal CtgJijianXishu { get; set; } // 车头工计件系数 + public decimal GyzkLtwxKs { get; set; } // 工艺-联统误选颗数 + public decimal GyzkLtwxl { get; set; } // 工艺-联统误选率 + public decimal XjChanliangPrice { get; set; } // 选茧产量单价 +} + +/// 薪资配置-工种/岗位(按 工种 唯一) +[Table(Name = "ConfGzs")] +[Index("IX_ConfGzs_WorkTypeId", "WorkTypeId", IsUnique = true)] +public class ConfGz : BaseEntity +{ + public long WorkTypeId { get; set; } // 工种/岗位 + + public decimal HoursPerday { get; set; } // 标准日工作时长 + public decimal QqDaysStandard { get; set; } // 全勤-标准出勤天数 + public decimal QqHoursMaxQj { get; set; } // 全勤-最大请假时长 + public decimal QqHoursDkMinjiaban { get; set; } // 全勤-满勤时请假免扣全勤最小加班时长 + public decimal QqHoursDkQingjia { get; set; } // 全勤-超过加班天数后免扣全勤请假时长 + + public decimal XgJineQuanqin { get; set; } // 学工-全勤奖金额 + public decimal XgJineGangwei { get; set; } // 学工-岗位薪资 + + public decimal JineNiangong { get; set; } // 年功工资 + public decimal JineQuanqin { get; set; } // 全勤奖金额 + public decimal JineGangwei { get; set; } // 岗位薪资 + public decimal JineYuekhJixiao { get; set; } // 月度绩效考核金额 + public decimal JineGwkh { get; set; } // 岗位考核 + public decimal JineYueticheng { get; set; } // 月度提成 + public decimal JineJtkhYear { get; set; } // 年度考核计提 + public decimal JineJisuanjishu { get; set; } // 计算基数工资 + public decimal JineYeban { get; set; } // 夜班补助金额/次 + public decimal JineJiaotongButie { get; set; } // 交通补贴/天 + public decimal LiudongJineGuding { get; set; } // 固定流动工资/小时 + + public decimal JiaqiJineKuangongKk { get; set; } // 旷工扣除/小时 + public decimal JiaqiJineFadingGz { get; set; } // 法定假工资/小时 + public decimal JiaqiJineFadingBz { get; set; } // 法定假补助/小时 + public decimal JiaqiJineHunjiaBz { get; set; } // 婚假补助/小时 + public decimal JiaqiJineSangjiaBz { get; set; } // 丧假补助/小时 + public decimal JiaqiJineBingjiaBz { get; set; } // 病假补助/小时 + public decimal JiaqiJineLunxiuBz { get; set; } // 轮休补助/小时 + public decimal JiabanJineBz { get; set; } // 加班补助/小时 + public decimal Jiaqi_Jine_TiaoxiuBz { get; set; } // 调休补助 + public decimal Jiaqi_Jine_NianjiaBz { get; set; } // 年假补助 + + public decimal GwsrkhJineQianhege { get; set; } // 岗位胜任考核-欠合格 + public decimal GwsrkhJineQiwang { get; set; } // 岗位胜任考核-期望 + public decimal GwsrkhJineHege { get; set; } // 岗位胜任考核-合格 + public decimal GwsrkhJineShengren { get; set; } // 岗位胜任考核-胜任 + public decimal GwsrkhJineChaoshengren { get; set; } // 岗位胜任考核-超胜任 + + public decimal Jiaban_GwsrkhJineQianhege { get; set; } // 加班工资-欠合格 + public decimal Jiaban_GwsrkhJineQiwang { get; set; } // 加班工资-期望 + public decimal Jiaban_GwsrkhJineHege { get; set; } // 加班工资-合格 + public decimal Jiaban_GwsrkhJineShengren { get; set; } // 加班工资-胜任 + public decimal Jiaban_GwsrkhJineChaoshengren { get; set; } // 加班工资-超胜任 +} + +/// 薪资配置-挡车/车头岗位(按 部门 唯一) +[Table(Name = "ConfDcgs")] +[Index("IX_ConfDcgs_DepId", "DepId", IsUnique = true)] +public class ConfDcg : BaseEntity +{ + public long DepId { get; set; } // 部门 + public decimal JineJisuanjishu50 { get; set; } // 50绪计算基数工资 + public decimal JineJisuanjishu60 { get; set; } // 60绪计算基数工资 + public decimal JineJisuanjishu70 { get; set; } // 70绪计算基数工资 + public decimal JineJisuanjishu100 { get; set; } // 100绪计算基数工资 + public decimal JineCtg { get; set; } // 车头工金额 +} + +/// 挡车纤度奖扣(按 规格 + 纤度 唯一) +[Table(Name = "DangCheXianDuJiangKous")] +[Index("IX_Dcxdjk_Guige_Xiandu", "GuiGe, XianDu", IsUnique = true)] +public class DangCheXianDuJiangKou : BaseEntity +{ + public string GuiGe { get; set; } = ""; // 规格 + public decimal XianDu { get; set; } // 纤度 + public decimal JiangLi { get; set; } // 奖励 + public decimal KouKuan { get; set; } // 扣款 +} + +/// 薪资价格配置(按 分组 + 项目 唯一) +[Table(Name = "XinZiJiaGePeiZhis")] +[Index("IX_Xzjg_Group_Key", "Group, Key", IsUnique = true)] +public class XinZiJiaGePeiZhi : BaseEntity +{ + public string Group { get; set; } = ""; // 分组(如「薪资.前缫」) + public string Key { get; set; } = ""; // 项目(如「挡车工计件等级奖扣单价」) + public string Unit { get; set; } = ""; // 单位 + public decimal Price { get; set; } // 价格 + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/PayRoll/ConfEmployee.cs b/backend/F9WebAiTest.Domain/Entities/PayRoll/ConfEmployee.cs new file mode 100644 index 0000000..b5c8f14 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/PayRoll/ConfEmployee.cs @@ -0,0 +1,45 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.PayRoll; + +// 考勤统计(CQTJ)依赖的三套配置: +// CONFZQ 薪资配置-账期、CONFEMPLOYEE 薪资配置-职工、GWsrkh 岗位胜任考核 +// 对应 F9 WinClient\F9.Business.PayRoll\PAYROLL\CONFZQ / CONFEMPLOYEE 与 CalXzMain 的岗位胜任考核。 + +/// 薪资配置-账期(按 年度 + 月份 唯一):考勤统计用「前缫出勤天数 QsQuanqinDays」判断满勤 +[Table(Name = "ConfZqs")] +[Index("IX_ConfZqs_Year_Month", "Year, Month", IsUnique = true)] +public class ConfZq : BaseEntity +{ + public int Year { get; set; } + public int Month { get; set; } + public decimal QsQuanqinDays { get; set; } // 前缫出勤天数(必填,考勤统计满勤/加班免扣口径) + public string Memo { get; set; } = ""; +} + +/// 薪资配置-职工(按 职工 + 工种 唯一):法定假补助/工资小时数、是否学工 +[Table(Name = "ConfEmployees")] +[Index("IX_ConfEmployees_Emp_WorkType", "EmployeeId, WorktypeId", IsUnique = true)] +public class ConfEmployee : BaseEntity +{ + public long EmployeeId { get; set; } + public long WorktypeId { get; set; } + public decimal HoursFadingBz { get; set; } // 法定假-补助小时 + public decimal HoursFadingGz { get; set; } // 法定假-工资小时 + public bool IsXuegong { get; set; } // 是否学工(全勤奖取学工标准) + public string Memo { get; set; } = ""; +} + +/// 岗位胜任考核(按 年度+月份+职工 唯一):考核结果决定加班费单价档位 +[Table(Name = "Gwsrkhs")] +[Index("IX_Gwsrkhs_Year_Month_Emp", "Year, Month, EmployeeId", IsUnique = true)] +public class Gwsrkh : BaseEntity +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public long EmployeeId { get; set; } + public string KhJieguo { get; set; } = ""; // 欠合格 / 期望 / 合格 / 胜任 / 超胜任 + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Cds.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Cds.cs new file mode 100644 index 0000000..f5508b4 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Cds.cs @@ -0,0 +1,32 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 秤大丝-落丝汇总(每个落丝记录一条) +[Table(Name = "CdsLsjls")] +[Index("IX_CdsLsjls_LsjlId", "LsjlId", IsUnique = true)] +public class CdsLsjl : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Num { get; set; } // 已秤数(把数) + public int Siliang { get; set; } // 已秤丝量(g) + public int Jianliang { get; set; } // 茧量(g)= 落丝桶数 × 每桶茧量 + public decimal Saozhe { get; set; } // 缫折 = 茧量 / 丝量 × 100 + public decimal Taichan { get; set; } // 实绩台产 = 丝量 / 落丝时长 / 车台数 +} + +/// 秤大丝-车台明细(每车台一行,4 个丝量值,单位 g) +[Table(Name = "CdsChetais")] +[Index("IX_CdsChetais_LsjlId_Chetaihao", "LsjlId, Chetaihao", IsUnique = true)] +public class CdsChetai : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public int Chetaihao { get; set; } // 车台号 + public int Sl1 { get; set; } // 丝量1 + public int Sl2 { get; set; } // 丝量2 + public int Sl3 { get; set; } // 丝量3 + public int Sl4 { get; set; } // 丝量4 + public int Num { get; set; } // 个数 = 丝量 > 0 的个数 + public int Siliang { get; set; } // 小计丝量 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Cqdj.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Cqdj.cs new file mode 100644 index 0000000..4f28005 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Cqdj.cs @@ -0,0 +1,55 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// +/// 考勤登记主表:一人一天一条(唯一:日期 + 职工)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ 的 Cqdj 实体。 +/// +[Table(Name = "biz_prod_kq_cqdj")] +public class Cqdj : BaseEntity +{ + public DateTime Date { get; set; } // 考勤日期 + public long DepId { get; set; } // 部门(车间) + public long WorktypeId { get; set; } // 工种 + public long EmployeeId { get; set; } // 职工 + public int Banci { get; set; } // 班次(Prod_Banci_Enum:甲=1 乙=2) + public bool IsXuegong { get; set; } // 是否学工(未转正) + + public decimal HoursStandard { get; set; } // 标准时长 + public decimal HoursCq { get; set; } // 出勤时长(含正常/流动/代班/外派/工伤,不含加班) + public decimal HoursQj { get; set; } // 请假时长 + public decimal HoursLd { get; set; } // 流动时长 + public decimal HoursJb { get; set; } // 加班时长 + public decimal HoursDb { get; set; } // 代班时长 + public decimal HoursWp { get; set; } // 外派时长 + public decimal HoursKg { get; set; } // 旷工时长 + public decimal HoursGs { get; set; } // 工伤时长 + + public int CsYeban { get; set; } // 夜班次数 + public string Memo { get; set; } = ""; // 备注 +} + +/// +/// 考勤登记明细:一人一天多行,按出勤类型拆分。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQDJ 的 Cqdj_Item 实体。 +/// +[Table(Name = "biz_prod_kq_cqdj_item")] +public class CqdjItem : BaseEntity +{ + public long CqdjId { get; set; } // 主表 Id + public DateTime Date { get; set; } // 考勤日期 + public long DepId { get; set; } // 出勤部门(流动/代班时可能是别的部门) + public long WorktypeId { get; set; } // 出勤工种 + public long EmployeeId { get; set; } // 职工 + public int Banci { get; set; } // 班次 + public int TypeCq { get; set; } // 出勤类型(Cqdj_CqType_Enum) + public int TypeQj { get; set; } // 请假类型(Cqdj_QjType_Enum,非请假为 0) + public decimal Hours { get; set; } // 时长 + public string Jizu { get; set; } = ""; // 机组 + public string CtStart { get; set; } = ""; // 开始车台 + public string CtEnd { get; set; } = ""; // 结束车台 + public int DangcheNum { get; set; } // 挡车数 + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Cqtj.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Cqtj.cs new file mode 100644 index 0000000..07cd561 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Cqtj.cs @@ -0,0 +1,74 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// +/// 考勤统计(月快照,一人一行)。 +/// 对应 F9 WinClient\F9.Business.Prod\KQ\CQTJ 的 Cqtj 实体。 +/// 数据由「录入 / 一键生成」按账期从考勤登记(Cqdj + CqdjItem)汇总后落库, +/// 落库后可在统计页手工调整(年假 / 公假 / 调休 / 法定假 / 全勤相关时长与金额等)。 +/// +[Table(Name = "biz_prod_kq_cqtj")] +[Index("IX_Cqtj_Year_Month_DepId", "Year, Month, DepId", false)] +public class Cqtj : BaseEntity +{ + public int Year { get; set; } + public int Month { get; set; } + public long DepId { get; set; } + public long WorktypeId { get; set; } + public long EmployeeId { get; set; } + + // ===== 出勤天数 ===== + public decimal DaysCqStandard { get; set; } // 标准出勤天数(工种配置 QqDaysStandard) + public decimal DaysCqActual { get; set; } // 实际出勤天数 = 本部门出勤时长 / HoursPerday + public decimal CqTianci { get; set; } // 出勤天次:(出勤 + 加班) > 0 的天数 + + // ===== 时长 ===== + public decimal HoursCq { get; set; } // 出勤 + public decimal HoursQj { get; set; } // 请假(合计) + public decimal HoursLd { get; set; } // 流动 + public decimal HoursJb { get; set; } // 加班 + public decimal HoursDb { get; set; } // 代班 + public decimal HoursWp { get; set; } // 外派 + public decimal HoursKg { get; set; } // 旷工 + public int CsYeban { get; set; } // 夜班次数 + + // ===== 请假分项时长 ===== + public decimal HoursQjShijia { get; set; } // 事假 + public decimal HoursQjBj { get; set; } // 病假 + public decimal HoursQjHj { get; set; } // 婚假 + public decimal HoursQjCj { get; set; } // 产假 + public decimal HoursQjLx { get; set; } // 轮休 + public decimal HoursQjSangjia { get; set; } // 丧假 + public decimal HoursQjGj { get; set; } // 公假 + public decimal HoursQjNj { get; set; } // 年假 + public decimal HoursQjTx { get; set; } // 调休 + + // ===== 学工时长 ===== + public decimal HoursXgCq { get; set; } // 学工出勤 + public decimal HoursXgJiaban { get; set; } // 学工加班 + + // ===== 法定假 / 全勤相关时长 ===== + public decimal HoursFadingjiaqi { get; set; } // 法定假期时长 = 职工配置 法定补助 + 法定工资 + public decimal HoursQjQqmc { get; set; } // 全勤免扣请假(加班免扣) + public decimal HoursQjQqcal { get; set; } // 全勤计算请假(本部门) + + // ===== 金额 ===== + public decimal JineQuanqin { get; set; } // 全勤奖 + public decimal JineJb { get; set; } // 加班费 + public decimal JineLiudong { get; set; } // 流动工资 + public decimal JineDaiban { get; set; } // 代班工资 + public decimal JineYb { get; set; } // 夜班补助 + public decimal JineKgKk { get; set; } // 旷工考核 + + public decimal JineJqBzHj { get; set; } // 婚假补助 + public decimal JineJqBzBj { get; set; } // 病假补助 + public decimal JineJqBzLx { get; set; } // 轮休补助 + public decimal JineJqBzSangjia { get; set; } // 丧假补助 + public decimal JineJqBzFading { get; set; } // 法定假补助 + public decimal JineJqBzTxnj { get; set; } // 调休 + 年假补助 + public decimal JineJqBz { get; set; } // 假期补助合计 + + public string Memo { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Glxd.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Glxd.cs new file mode 100644 index 0000000..3107630 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Glxd.cs @@ -0,0 +1,13 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 公量纤度(每个落丝记录一条,由质检录入) +[Table(Name = "Glxds")] +[Index("IX_Glxds_LsjlId", "LsjlId", IsUnique = true)] +public class Glxd : BaseEntity +{ + public long LsjlId { get; set; } // 落丝记录 + public decimal Gongliangxiandu { get; set; } // 公量纤度 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs b/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs index f01ffc2..3525b1f 100644 --- a/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs +++ b/backend/F9WebAiTest.Domain/Entities/Prod/LsjlRecord.cs @@ -20,6 +20,8 @@ public class LsjlRecord : BaseEntity public int Chetaishu { get; set; } // 车台数 public bool Isbanche { get; set; } // 是否半车 public double Hours { get; set; } // 缫丝时长 + public int Tongshu { get; set; } // 落丝桶数(秤大丝「茧量」口径需要) + public int Meitongjianliang { get; set; } // 每桶茧量(g,秤大丝「茧量」口径需要) public string Guige { get; set; } = ""; // 规格 public double Chesu { get; set; } // 车速 } diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Spjc.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Spjc.cs new file mode 100644 index 0000000..8eef1d1 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Spjc.cs @@ -0,0 +1,46 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 丝片检查(按「日期 + 车间 + 工艺庄口 + 规格」唯一) +[Table(Name = "Spjcs")] +[Index("IX_Spjcs_Date_DepId_GyzkId_Guige", "Date, DeptId, GyzkId, Guige", IsUnique = true)] +public class Spjc : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long DeptId { get; set; } // 车间 + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + + public int Pianshu { get; set; } // 总片数 + public int PianshuXuegong { get; set; } // 学工片数 + + // ---- 前缫 ---- + public int QsJiahua { get; set; } // 夹花 + public int QsYouwu { get; set; } // 油污 + public int QsHeidian { get; set; } // 黑点 + public int QsShangsi { get; set; } // 伤丝 + public int QsYetiao { get; set; } // 野条 + public int QsXiaosipian { get; set; } // 小丝片 + public int QsQita { get; set; } // 其他 + public int QsHeji { get; set; } // 合计 + + // ---- 后缫 ---- + public int HsYouwu { get; set; } // 油污 + public int HsShangsi { get; set; } // 伤丝 + public int HsShuangsi { get; set; } // 双丝 + public int HsZhisi { get; set; } // 直丝 + public int HsZhantiao { get; set; } // 粘条 + public int HsYingjiao { get; set; } // 硬胶 + public int HsGuojiao { get; set; } // 过胶 + public int HsShouchu { get; set; } // 手触 + public int HsWuran { get; set; } // 污染 + public int HsLiansi { get; set; } // 连丝 + public int HsWdmt { get; set; } // 无底面头 + public int HsMaoluan { get; set; } // 毛乱 + public int HsQita { get; set; } // 其他 + public int HsHeji { get; set; } // 合计 + + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Sptz.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Sptz.cs new file mode 100644 index 0000000..220bf6b --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Sptz.cs @@ -0,0 +1,29 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +/// 丝片台帐(每日一条):浸丝 / 返丝 / 编丝 / 整理 +[Table(Name = "Sptzs")] +[Index("IX_Sptzs_Date", "Date", IsUnique = true)] +public class Sptz : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public DateTime DateLast { get; set; } // 上一日(上一本台账日期) + + // ---- 浸丝 ---- + public int JsShangcun { get; set; } // 上存 = 上一日今存 + public int JsXiasi { get; set; } // 下丝 + public int JsJinsi { get; set; } // 浸丝 + public int JsSrylJrxz { get; set; } // 上日遗留/今日新增 = 上存 − 浸丝 + public int JsJincun { get; set; } // 今存 = 下丝 + 上日遗留/今日新增 + + public int FsFsps { get; set; } // 返丝片数 + public int BsBsps { get; set; } // 编丝片数 + + // ---- 整理 ---- + public int ZlZhengpin { get; set; } // 正品片数 + public int ZlTichu { get; set; } // 剔除片数 + + public string Memo { get; set; } = ""; // 备注 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/XjpbBanzu.cs b/backend/F9WebAiTest.Domain/Entities/Prod/XjpbBanzu.cs new file mode 100644 index 0000000..44b4c78 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/XjpbBanzu.cs @@ -0,0 +1,32 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +// 选茧班组(对应 F9 生产-选剥茧 XBJ\XJBZ 两张表:班组 / 班组职工) +// 数据口径: +// 1) XjpbBanzu 班组:【班次(甲/乙)+ 选茧组】一条,来自职工分配时勾选的组别。 +// 2) XjpbBanzuZhigong 班组职工:某班组下的职工及顺序,来源是「选剥茧车间」下工种为「选茧工」的在职职工。 +// 注意:老系统班组由「编辑录入」时勾选职工+组别后保存生成(XjpbBanzuZhigong_SaveCommand), +// 班组列表本身没有独立的新增入口,移植后同样以「编辑录入」批量保存为准。 + +/// 选茧班组(F9 biz_prod_xbj_xjpb_banzu) +[Table(Name = "XjpbBanzus")] +public class XjpbBanzu : BaseEntity +{ + public string Banci { get; set; } = ""; // 班次(甲 / 乙) + public int Zu { get; set; } // 选茧组 +} + +/// 选茧班组职工(F9 biz_prod_xbj_xjpb_banzu_zhigong) +[Table(Name = "XjpbBanzuZhigongs")] +public class XjpbBanzuZhigong : BaseEntity +{ + public long BanzuId { get; set; } // 选茧班组ID + public string Banci { get; set; } = ""; // 班次(甲 / 乙) + public int Zu { get; set; } // 选茧组 + public long YgId { get; set; } // 职工ID + public string YgName { get; set; } = ""; // 职工姓名 + public string YgPhone { get; set; } = ""; // 职工电话 + public int Sort { get; set; } // 顺序 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Xjrb.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Xjrb.cs new file mode 100644 index 0000000..96bbe86 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Xjrb.cs @@ -0,0 +1,113 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Prod; + +// 选茧日报(对应 F9 生产-选剥茧 XBJ\XJRB 三张表:日报主表 / 计划 / 计划明细) +// 数据口径: +// 1) XjrbRecord 日报:某日某工艺庄口一条,记录干茧领入与 8 类茧(联统/好五级/六级/绵/双宫/下脚/茧衣/茧花)的 +// 包数与重量,另含「退回」、合计重量、联统率、盈亏。 +// 2) XjrbPlan 计划:按领料单(原料出库单)下达的选茧计划,含计划的 8 类茧与选茧盈亏、是否完成。 +// 3) XjrbItem 计划明细:挂在计划下的每日选茧进度行(外键 XjrbPlanId),完成勾选后回写计划盈亏。 +// +// 盈亏 / 联统率口径(对齐 F9 GetXjrbCountYingkuiQuery): +// 累计区间 = 同庄口下 Date < 当前日期 的历史日报(含当前录入值) +// yllr = Σ 干茧领入重量(含当前) +// ltj = Σ 联统茧重量(含当前) +// all = Σ (联统+好五级+六级+绵+双宫+下脚+茧衣+茧花) 重量(含当前,不含退回) +// 联统率 = yllr != 0 ? (ltj / yllr) * 100 : 0 +// 盈亏 = all - yllr + +/// 选茧日报主表(F9 biz_prod_xbj_xjrb) +[Table(Name = "XjrbRecords")] +public class XjrbRecord : BaseEntity +{ + public DateTime Date { get; set; } // 选茧日期 + public long GyzkId { get; set; } // 工艺庄口 + + public int YlLrBaoshu { get; set; } // 干茧领入包数 + public decimal YlLrZhongliang { get; set; } // 干茧领入重量 + + public int LtjBaoshu { get; set; } // 联统茧包数 + public decimal LtjZhongliang { get; set; } // 联统茧重量 + public int HwjjBaoshu { get; set; } // 好五级茧包数 + public decimal HwjjZhongliang { get; set; } // 好五级茧重量 + public int LjjBaoshu { get; set; } // 六级茧包数 + public decimal LjjZhongliang { get; set; } // 六级茧重量 + public int MjBaoshu { get; set; } // 绵茧包数 + public decimal MjZhongliang { get; set; } // 绵茧重量 + public int SgjBaoshu { get; set; } // 双宫茧包数 + public decimal SgjZhongliang { get; set; } // 双宫茧重量 + public int XjjBaoshu { get; set; } // 下脚茧包数 + public decimal XjjZhongliang { get; set; } // 下脚茧重量 + public int JyBaoshu { get; set; } // 茧衣包数 + public decimal JyZhongliang { get; set; } // 茧衣重量 + public int JhBaoshu { get; set; } // 茧花包数 + public decimal JhZhongliang { get; set; } // 茧花重量 + + public decimal TotalZhongliang { get; set; } // 合计重量(上述 8 项重量之和) + + public int ThBaoshu { get; set; } // 退回包数 + public decimal ThZhongliang { get; set; } // 退回重量 + + public decimal LiantongRate { get; set; } // 联统率(%) + public decimal Yingkui { get; set; } // 盈亏 +} + +/// 选茧计划(F9 biz_prod_xbj_xjrb_plan):按领料单下达,含计划盈亏与完成标记 +[Table(Name = "XjrbPlans")] +public class XjrbPlan : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long GyzkId { get; set; } // 工艺庄口 + public long YllingliaodanId { get; set; } // 领料单(原料出库单)ID + + public int YlBaoshu { get; set; } // 原料领入包数 + public decimal YlZhongliang { get; set; } // 原料领入重量 + + public int LtjBaoshu { get; set; } // 联统茧包数 + public decimal LtjZhongliang { get; set; } // 联统茧重量 + public int HwjjBaoshu { get; set; } // 好五级茧包数 + public decimal HwjjZhongliang { get; set; } // 好五级茧重量 + public int WjjBaoshu { get; set; } // 五级茧包数 + public decimal WjjZhongliang { get; set; } // 五级茧重量 + public int SgjBaoshu { get; set; } // 双宫茧包数 + public decimal SgjZhongliang { get; set; } // 双宫茧重量 + public int XjjBaoshu { get; set; } // 下脚茧包数 + public decimal XjjZhongliang { get; set; } // 下脚茧重量 + public int JyBaoshu { get; set; } // 茧衣包数 + public decimal JyZhongliang { get; set; } // 茧衣重量 + public int JhBaoshu { get; set; } // 茧花包数 + public decimal JhZhongliang { get; set; } // 茧花重量 + + public decimal TotalZhongliang { get; set; } // 合计重量 + public decimal LiantongRate { get; set; } // 联统率(%) + public decimal Yingkui { get; set; } // 选茧盈亏 + public bool IsFinish { get; set; } // 是否选茧完成(完成后禁止改动明细) +} + +/// 选茧计划明细(F9 biz_prod_xbj_xjrb_item):挂在计划下的每日选茧进度行 +[Table(Name = "XjrbItems")] +public class XjrbItem : BaseEntity +{ + public long XjrbPlanId { get; set; } // 选茧计划ID + public DateTime Date { get; set; } // 选茧日期 + + public int LtjBaoshu { get; set; } // 联统茧包数 + public decimal LtjZhongliang { get; set; } // 联统茧重量 + public int HwjjBaoshu { get; set; } // 好五级茧包数 + public decimal HwjjZhongliang { get; set; } // 好五级茧重量 + public int WjjBaoshu { get; set; } // 五级茧包数 + public decimal WjjZhongliang { get; set; } // 五级茧重量 + public int SgjBaoshu { get; set; } // 双宫茧包数 + public decimal SgjZhongliang { get; set; } // 双宫茧重量 + public int XjjBaoshu { get; set; } // 下脚茧包数 + public decimal XjjZhongliang { get; set; } // 下脚茧重量 + public int JyBaoshu { get; set; } // 茧衣包数 + public decimal JyZhongliang { get; set; } // 茧衣重量 + public int JhBaoshu { get; set; } // 茧花包数 + public decimal JhZhongliang { get; set; } // 茧花重量 + + public decimal TotalZhongliang { get; set; } // 合计重量 + public bool IsFinish { get; set; } // 是否选茧完成(勾选后回写计划盈亏) +} diff --git a/backend/F9WebAiTest.Domain/Entities/Prod/Zjrb.cs b/backend/F9WebAiTest.Domain/Entities/Prod/Zjrb.cs new file mode 100644 index 0000000..ffda436 --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Prod/Zjrb.cs @@ -0,0 +1,79 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; +using F9WebAiTest.Domain.Enums.SYS.Employees; + +namespace F9WebAiTest.Domain.Entities.Prod; + +// 煮茧日报(对应 F9 生产-前缫 的 ZJRB 四张表:日报主表 / 机组 / 班次 / 机组班次) +// 数据口径:某日某车间某庄口某规格一条日报,主表存「班次合计」,机组存桶数,机组班次存甲乙班今缫桶数。 + +/// 煮茧日报主表(biz_prod_qs_zjrb) +[Table(Name = "ZjrbRecords")] +public class ZjrbRecord : BaseEntity +{ + public DateTime Date { get; set; } // 日期 + public long DeptId { get; set; } // 车间(前缫) + public long GyzkId { get; set; } // 工艺庄口 + public string Guige { get; set; } = ""; // 规格 + public DateTime? LastDate { get; set; } // 上一日(写入时标记,取上一日一律按 Date < 当前日期) + public int StandardLuositongshu { get; set; } // 工艺落丝桶数(标准落丝桶数) + public int MeitongJianliang { get; set; } // 每桶茧量(g) + + // 包数(今日 = 甲班 + 乙班) + public int BaoshuShangcun { get; set; } // 上存包数 + public int BaoshuLingru { get; set; } // 领入包数 + public int BaoshuSaosi { get; set; } // 今缫包数 + public int BaoshuTuihui { get; set; } // 退回包数 + public int BaoshuJincun { get; set; } // 今存包数 + + // 重量(kg,今日 = 甲班 + 乙班) + public decimal JianliangShangcun { get; set; } // 上存重量 + public decimal JianliangLingru { get; set; } // 领入重量 + public decimal JianliangSaosi { get; set; } // 今缫重量 + public decimal JianliangTuihui { get; set; } // 退回重量 + public decimal JianliangJincun { get; set; } // 今存重量 +} + +/// 煮茧日报-机组(biz_prod_qs_zjrb_jizu):今存桶数 = 上存 + 今缫 - 今落 +[Table(Name = "ZjrbJizus")] +public class ZjrbJizu : BaseEntity +{ + public long ZjrbId { get; set; } // 煮茧日报ID + public int Jizu { get; set; } // 机组 + public decimal JizuSaosiHours { get; set; } // 缫丝时长(小时,上限 20) + public int TongshuShangcun { get; set; } // 上存桶数 + public int TongshuJinsao { get; set; } // 今缫桶数(= 甲班 + 乙班) + public int TongshuJinluo { get; set; } // 今落桶数 + public int TongshuJincun { get; set; } // 今存桶数 +} + +/// 煮茧日报-班次(biz_prod_qs_zjrb_banci):车间级甲乙班的包数/重量 +[Table(Name = "ZjrbBancis")] +public class ZjrbBanci : BaseEntity +{ + public long ZjrbId { get; set; } // 煮茧日报ID + public Prod_Banci_Enum Banci { get; set; } // 班次(甲/乙) + + public int BaoshuShangcun { get; set; } // 上存包数 + public int BaoshuLingru { get; set; } // 领入包数 + public int BaoshuSaosi { get; set; } // 今缫包数 + public int BaoshuTuihui { get; set; } // 退回包数 + public int BaoshuJincun { get; set; } // 今存包数 + + public decimal JianliangShangcun { get; set; } // 上存重量 + public decimal JianliangLingru { get; set; } // 领入重量 + public decimal JianliangSaosi { get; set; } // 今缫重量 + public decimal JianliangTuihui { get; set; } // 退回重量 + public decimal JianliangJincun { get; set; } // 今存重量 +} + +/// 煮茧日报-机组班次(biz_prod_qs_zjrb_jizu_banci):只存每机组每班次的今缫桶数 +[Table(Name = "ZjrbJizuBancis")] +public class ZjrbJizuBanci : BaseEntity +{ + public long ZjrbId { get; set; } // 煮茧日报ID + public long ZjrbJizuId { get; set; } // 煮茧日报机组ID + public int Jizu { get; set; } // 机组 + public Prod_Banci_Enum Banci { get; set; } // 班次(甲/乙) + public int TongshuJinsao { get; set; } // 今缫桶数 +} diff --git a/backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs b/backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs new file mode 100644 index 0000000..7996c8a --- /dev/null +++ b/backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs @@ -0,0 +1,378 @@ +using FreeSql.DataAnnotations; +using F9WebAiTest.Domain.Common; + +namespace F9WebAiTest.Domain.Entities.Wms; + +// 仓储(对应 F9 WinClient\F9.Business.WMS 的五个模块:原料 / 副产品 / 中间品 / 生丝 / 物资) +// 五个模块的「入库 / 出库 / 库存台账」结构完全一致,仅「台账项目」口径不同: +// 原料 = 原料庄口,副产品/中间品/生丝/物资 = 物资目录(中间品另有一套按工艺庄口的「笼筒架台账」) +// 因此统一为一套表 + Module 维度(F9 里是五套同名表),字段为五个模块字段的并集。 + +/// 仓储模块 +public enum Wms_Module_Enum +{ + 原料 = 1, + 副产品 = 2, + 中间品 = 3, + 生丝 = 4, + 物资 = 5 +} + +/// 入库单(五模块共用) +[Table(Name = "WmsRukus")] +[Index("IX_WmsRukus_Module_Date_Code", "Module, Date, Code", IsUnique = true)] +public class WmsRuku : BaseEntity +{ + public Wms_Module_Enum Module { get; set; } // 模块 + public DateTime Date { get; set; } // 入库日期 + public int Type { get; set; } // 入库类型(各模块枚举值,见 WmsTypes) + public string Code { get; set; } = ""; // 单据编号 + public long GysId { get; set; } // 供应商(原料/物资) + public long KhId { get; set; } // 客户(副产品) + public long GyzkId { get; set; } // 工艺庄口(副产品入库) + public long DepId { get; set; } // 部门(副产品/在制品) + public string Lianxiren { get; set; } = ""; // 联系人(原料) + public string Phone { get; set; } = ""; // 联系电话(原料) + public string Carno { get; set; } = ""; // 车牌号(原料) + public string Kuguan { get; set; } = ""; // 库管员 + public string Caigouyuan { get; set; } = ""; // 采购员(物资) + public string PayStatus { get; set; } = ""; // 支付类型(物资) + public int RkStatus { get; set; } // 入库状态(物资:0未入库 1已入库) + public int BxStatus { get; set; } // 报销状态(物资:0无 1待报销 2已报销) + public string Description { get; set; } = ""; // 备注 +} + +/// 入库明细(五模块共用) +[Table(Name = "WmsRukuDetails")] +public class WmsRukuDetail : BaseEntity +{ + public long RukuId { get; set; } + public long WzmlId { get; set; } // 物资目录 + public long YlzkId { get; set; } // 原料庄口(原料) + public long GyzkId { get; set; } // 工艺庄口(在制品) + public long DepId { get; set; } // 部门(在制品) + public int Jianshu { get; set; } // 件数 + public decimal Maozhong { get; set; } // 毛重 + public decimal Pizhong { get; set; } // 皮重 + public decimal Jingzhong { get; set; } // 净重 + public decimal BiaobaoZhongliang { get; set; } // 标包重量(在制品) + public decimal BaozhuangZhongliang { get; set; } // 包装重量(在制品) + public decimal Shuliang { get; set; } // 数量(物资) + public decimal Danjia { get; set; } // 单价 + public decimal Jine { get; set; } // 金额 + public decimal Sunhao { get; set; } // 运损(原料) + public string PiciCodes { get; set; } = ""; // 批次号(生丝) + public int PiciNum { get; set; } // 批数(生丝) + public int BaojianNum { get; set; } // 包件数(生丝) + public string PiciIds { get; set; } = ""; // 批次ID(生丝,逗号分隔,入库按批次关联) + public string Banci { get; set; } = ""; // 班次(在制品出库) + public string JidiWaigou { get; set; } = ""; // 基地 / 外购(生丝) +} + +/// 磅码单(原料入库/出库过磅明细) +[Table(Name = "WmsBangmadans")] +public class WmsBangmadan : BaseEntity +{ + public long DetailId { get; set; } // 出/入库明细 Id + public bool IsChuku { get; set; } // false=入库 true=出库 + public int Jianshu { get; set; } // 件数 + public decimal Maozhong { get; set; } // 毛重 + public decimal Pizhong { get; set; } // 皮重 + public decimal Jingzhong { get; set; } // 净重 = 毛重 − 皮重 + public DateTime? GuobangShijian { get; set; } // 过磅时间 +} + +/// 出库单(五模块共用) +[Table(Name = "WmsChukus")] +[Index("IX_WmsChukus_Module_Date_Code", "Module, Date, Code", IsUnique = true)] +public class WmsChuku : BaseEntity +{ + public Wms_Module_Enum Module { get; set; } + public DateTime Date { get; set; } // 出库日期 + public int Type { get; set; } // 出库类型 + public string Code { get; set; } = ""; // 单据编号 + public long WldwId { get; set; } // 往来单位/客户(生丝/原料销售) + public long KhId { get; set; } // 客户(副产品) + public long GyzkId { get; set; } // 工艺庄口(原料领料) + public long LingliaoDepId { get; set; } // 领料部门(物资) + public long UseDepId { get; set; } // 使用部门(物资) + public long DbdepId { get; set; } // 调拨部门(物资/副产品) + public string Lingliaoren { get; set; } = ""; // 领料人 + public string Kuguan { get; set; } = ""; // 库管员 + public string UseLocation { get; set; } = ""; // 使用位置(物资) + public int CkStatus { get; set; } // 出库状态(物资:0未出库 1已出库) + public DateTime? CkTime { get; set; } // 出库时间(物资) + public bool IsHongchong { get; set; } // 是否红冲(物资) + public long HongchongId { get; set; } // 红冲单据(物资) + public string Description { get; set; } = ""; // 备注 +} + +/// 出库明细(五模块共用) +[Table(Name = "WmsChukuDetails")] +public class WmsChukuDetail : BaseEntity +{ + public long ChukuId { get; set; } + public long WzmlId { get; set; } // 物资目录 + public long YlzkId { get; set; } // 原料庄口(原料) + public long GyzkId { get; set; } // 工艺庄口(在制品) + public long DepId { get; set; } // 部门(在制品) + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal BiaobaoZhongliang { get; set; } + public decimal BaozhuangZhongliang { get; set; } + public decimal Shuliang { get; set; } // 数量(物资) + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public decimal KucunNum { get; set; } // 库存数量(物资,用于金额分摊) + public string PiciCodes { get; set; } = ""; // 批次号(生丝) + public int PiciNum { get; set; } + public string PcAndBjIds { get; set; } = ""; // 批次+包件ID(生丝,JSON:[{piciId,piciBaojianIds}...]) + public string Lingyongren { get; set; } = ""; // 领用人(物资) + public string Banci { get; set; } = ""; // 班次(在制品) + public string JidiWaigou { get; set; } = ""; // 基地 / 外购(生丝) +} + +/// 库存台账-日期(一天一条;在制品的「笼筒架台账」用 Kind=1 区分) +[Table(Name = "WmsKcTzDates")] +[Index("IX_WmsKcTzDates_Module_Kind_Date", "Module, Kind, Date", IsUnique = true)] +public class WmsKcTzDate : BaseEntity +{ + public Wms_Module_Enum Module { get; set; } + public int Kind { get; set; } // 0=库存台账 1=笼筒架台账(在制品) + public DateTime Date { get; set; } // 台账日期 + public DateTime LastDate { get; set; } // 上一台账日期 +} + +/// 库存台账-明细(按台账项目一行) +[Table(Name = "WmsKcTzItems")] +[Index("IX_WmsKcTzItems_TzDateId_ItemId", "TzDateId, ItemId, SubKey", IsUnique = true)] +public class WmsKcTzItem : BaseEntity +{ + public long TzDateId { get; set; } + public Wms_Module_Enum Module { get; set; } + public int Kind { get; set; } + public DateTime Date { get; set; } // 台账日期 + public long ItemId { get; set; } // 台账项目(原料=原料庄口,其余=物资目录,笼筒架=工艺庄口) + public string SubKey { get; set; } = ""; // 项目辅助维度(生丝=基地/外购) + public string ItemName { get; set; } = ""; // 项目名称快照 + public string ItemCode { get; set; } = ""; // 项目编号快照(物资台账) + public decimal Ydayjianshu { get; set; } // 上日结存件数(物资台账=数量) + public decimal Ydayjingzhong { get; set; } // 上日结存净重(物资台账=金额) + public decimal Tdayrukujianshu { get; set; } // 今日入库件数 + public decimal Tdayrukujingzhong { get; set; } // 今日入库净重 + public decimal Tdaychukujianshu { get; set; } // 今日出库件数 + public decimal Tdaychukujingzhong { get; set; } // 今日出库净重 + public decimal Yingkui { get; set; } // 盈亏 + public decimal Tdayjianshu { get; set; } // 今日结存件数 + public decimal Tdayjingzhong { get; set; } // 今日结存净重 + public decimal Yangjianjianshu { get; set; } // 样茧件数(原料) + public bool AllChuku { get; set; } // 全部出库 + public string Description { get; set; } = ""; +} + +/// 生丝批次(对应 F9 SS_Pici) +[Table(Name = "WmsPicis")] +[Index("IX_WmsPicis_Code", "Code", IsUnique = true)] +public class WmsPici : BaseEntity +{ + public string Code { get; set; } = ""; // 批号 + public int CodeType { get; set; } // 批号类型 + public int Year { get; set; } + public DateTime Date { get; set; } // 织批日期 + public DateTime? ScDate { get; set; } // 生产日期 + public long GyzkId { get; set; } // 工艺庄口 + public long WzmlId { get; set; } // 物资目录(生丝规格) + public long DepId { get; set; } // 部门 + public string Guige { get; set; } = ""; // 规格 + public string JidiWaigou { get; set; } = ""; // 基地 / 外购 + public string YoujiPutong { get; set; } = ""; // 优质 / 普通 + public int Baoshu { get; set; } // 包数 + public int StartBaojianNum { get; set; } // 起始包件号 + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } // 公量 + public int KcStatus { get; set; } // 0全部 1待入库 2已入库 3部分出库 4已出库 + public long RkItemId { get; set; } // 关联入库明细 + public DateTime? RkDate { get; set; } // 入库日期 + public long CkItemId { get; set; } // 关联出库明细(最后一笔) + public DateTime CkLastDate { get; set; } // 最后出库日期 + public int CkBaoshu { get; set; } // 已出库包数 + public decimal CkJingzhong { get; set; } // 已出库净重 + public int KcBaoshu { get; set; } // 库存包数 + public string Dengji { get; set; } = ""; // 等级 + public decimal Qingjie { get; set; } // 清洁 + public decimal Jiejing { get; set; } // 洁净 + public decimal Piancha { get; set; } // 偏差 + public decimal Zongcha { get; set; } // 总差 + public decimal Qieduan { get; set; } // 切断 + public string Description { get; set; } = ""; +} + +/// 生丝批次-包件(对应 F9 SS_Pici_Baojian) +[Table(Name = "WmsPiciBaojians")] +public class WmsPiciBaojian : BaseEntity +{ + public long PiciId { get; set; } + public int Baojianhao { get; set; } // 包件号 + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Gongliang { get; set; } // 公量 + public decimal Huichaolv { get; set; } // 实测回潮率(%) + public bool CkIsck { get; set; } // 是否已出库 + public long CkItemId { get; set; } // 出库明细ID + public DateTime? CkDate { get; set; } // 出库日期 + public decimal CkJingzhong { get; set; } // 出库净重 + public string Description { get; set; } = ""; +} + +/// 生丝批次-出库记录(对应 F9 SS_Pici_Chuku) +[Table(Name = "WmsPiciChukus")] +public class WmsPiciChuku : BaseEntity +{ + public long PiciId { get; set; } + public long CkItemId { get; set; } // 出库明细ID + public int CkBaoshu { get; set; } // 出库包数 + public DateTime? CkDate { get; set; } + public decimal CkJingzhong { get; set; } // 出库净重 +} + +/// 生丝批次-质检(对应 F9 SS_Pici_Qc) +[Table(Name = "WmsPiciQcs")] +[Index("IX_WmsPiciQcs_PiciId", "PiciId", IsUnique = true)] +public class WmsPiciQc : BaseEntity +{ + public long PiciId { get; set; } // 生丝批次ID + public string Pingwei { get; set; } = ""; // 品味 + public string Seze { get; set; } = ""; // 检验-色泽 + public decimal AvgHuichaolv { get; set; } // 平均回潮率 + public decimal AvgGongliangXiandu { get; set; } // 平均公量纤度 + public decimal XianduPiancha { get; set; } // 纤度偏差 + public decimal XianduZdpiancha { get; set; } // 纤度最大偏差 + public decimal Qingjie { get; set; } // 清洁 + public decimal Jiejing { get; set; } // 洁净 + public int JunyunYd { get; set; } // 均匀一度变化 + public int JunyunEd { get; set; } // 均匀二度变化 + public int JunyunSd { get; set; } // 均匀三度变化 + public int Qieduan { get; set; } // 切断次数 + public string DuanlieQiangdu { get; set; } = ""; // 断裂强度 + public string DuanlieShenchanglv { get; set; } = ""; // 断裂伸长率 + public int Baohe { get; set; } // 抱合 + public string WgWg { get; set; } = ""; // 外观-外观检验 + public string WgYanse { get; set; } = ""; // 外观-颜色 + public string WgGuangze { get; set; } = ""; // 外观-光泽 + public string WgShougan { get; set; } = ""; // 外观-手感 + public int? QjZhuyao { get; set; } // 清洁-主要疵点 + public int? QjCiyaoFeisi { get; set; } // 清洁-次要疵点-废丝 + public int? QjCiyaoDacao { get; set; } // 清洁-次要疵点-大糙 + public int? QjCiyaoNianfucao { get; set; } // 清洁-次要疵点-粘附糙 + public int? QjCiyaoDachangjie { get; set; } // 清洁-次要疵点-大长结 + public int? QjCiyaoZhongluoxuan { get; set; } // 清洁-次要疵点-重螺旋 + public int? QjPutongXiaocao { get; set; } // 清洁-普通疵点-小糙 + public int? QjPutongChangjie { get; set; } // 清洁-普通疵点-长结 + public int? QjPutongLuoxuan { get; set; } // 清洁-普通疵点-螺旋 + public int? QjPutongHuanjie { get; set; } // 清洁-普通疵点-环结及裂丝 + public int? Jj100 { get; set; } // 洁净100 + public int? Jj95 { get; set; } + public int? Jj90 { get; set; } + public int? Jj85 { get; set; } + public int? Jj80 { get; set; } + public int? Jj75 { get; set; } + public int? Jj70 { get; set; } + public int? Jj65 { get; set; } + public int? Jj60 { get; set; } + public int? Jj55 { get; set; } + public int? Jj50 { get; set; } + public int? Jj45 { get; set; } + public string JjYuanyin { get; set; } = ""; // 降级原因 + public string Description { get; set; } = ""; // 备注 +} + +/// 生丝批次-纤度检验(对应 F9 SS_Pici_Qc_Xdjy) +[Table(Name = "WmsPiciQcXdjys")] +public class WmsPiciQcXdjy : BaseEntity +{ + public long PiciId { get; set; } + public decimal Xiandu { get; set; } + public int Num { get; set; } +} + +/// 物资库存(对应 F9 WJ_Kucun,按物资目录一行) +[Table(Name = "WmsKucuns")] +[Index("IX_WmsKucuns_WzmlId", "WzmlId", IsUnique = true)] +public class WmsKucun : BaseEntity +{ + public long WzmlId { get; set; } + public decimal Shuliang { get; set; } // 库存数量 + public decimal Danjia { get; set; } // 库存单价(加权平均) + public decimal Jine { get; set; } // 库存金额 +} + +/// 原料采购(对应 F9 Yuanliao_Caigou) +[Table(Name = "WmsCaigous")] +public class WmsCaigou : BaseEntity +{ + public DateTime Date { get; set; } + public string Code { get; set; } = ""; // 采购单号 + public long GysId { get; set; } // 供应商(往来单位) + public long YlzkId { get; set; } // 原料庄口 + public int Jianshu { get; set; } + public decimal Maozhong { get; set; } + public decimal Pizhong { get; set; } + public decimal Jingzhong { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} + +/// 原料翻包计划(对应 F9 Fbplan) +[Table(Name = "WmsFbplans")] +public class WmsFbplan : BaseEntity +{ + public DateTime Date { get; set; } // 翻包日期 + public string Code { get; set; } = ""; // 计划号 + public long YlzkId { get; set; } // 原料庄口 + public long WzmlId { get; set; } // 物资目录 + public int Jianshu { get; set; } // 计划件数 + public decimal Jingzhong { get; set; } // 计划净重 + public int FbType { get; set; } // 1翻包中 2完成 3暂停 + public int RukuStatus { get; set; } // 1有效 2作废 3无 + public string Description { get; set; } = ""; +} + +/// 原料翻包记录(对应 F9 Fbjl) +[Table(Name = "WmsFbjls")] +public class WmsFbjl : BaseEntity +{ + public long FbplanId { get; set; } + public DateTime Date { get; set; } + public int Jianshu { get; set; } + public decimal Jingzhong { get; set; } + public string Description { get; set; } = ""; +} + +/// 物资采购计划(对应 F9 WJ_Cgplan) +[Table(Name = "WmsCgplans")] +public class WmsCgplan : BaseEntity +{ + public DateTime Date { get; set; } + public string Code { get; set; } = ""; // 计划号 + public long DepId { get; set; } // 申请部门 + public int Status { get; set; } // 0草稿 1已确认 2已完成 + public string Description { get; set; } = ""; +} + +/// 物资采购计划明细(对应 F9 WJ_Cgplan_Item) +[Table(Name = "WmsCgplanItems")] +public class WmsCgplanItem : BaseEntity +{ + public long CgplanId { get; set; } + public long WzmlId { get; set; } + public decimal Shuliang { get; set; } + public decimal Danjia { get; set; } + public decimal Jine { get; set; } + public string Description { get; set; } = ""; +} diff --git a/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs b/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs index 35a3aa6..8637ed3 100644 --- a/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs +++ b/backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs @@ -242,6 +242,20 @@ public static class BusinessSeedData } AddMenu(fsql, houxianDirId, "纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "EditPen", "prod:xdjy:list", 1); AddMenu(fsql, houxianDirId, "黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "Notebook", "prod:hbjy:list", 2); + AddMenu(fsql, houxianDirId, "秤大丝", "/prod/houxian/cds", "prod/cds/index", "Box", "prod:cds:list", 3); + AddMenu(fsql, houxianDirId, "公量纤度", "/prod/houxian/glxd", "prod/glxd/index", "TrendCharts", "prod:glxd:list", 4); + AddMenu(fsql, houxianDirId, "丝片检查", "/prod/houxian/spjc", "prod/spjc/index", "Document", "prod:spjc:list", 5); + AddMenu(fsql, houxianDirId, "丝片台帐", "/prod/houxian/sptz", "prod/sptz/index", "Files", "prod:sptz:list", 6); + + // 生产-选剥茧(对齐 F9 WinClient\F9.Business.Prod\XBJ):选茧日报 / 选茧计划 / 选茧班组 + var xuanjianDirId = fsql.Select().Where(m => m.Path == "/prod/xuanjian").First()?.Id ?? 0; + if (xuanjianDirId == 0) + { + xuanjianDirId = DbSeeder.InsertAndGetId(fsql, new Menu { Name = "生产-选剥茧", Type = "directory", Path = "/prod/xuanjian", Icon = "Grid", Sort = 7 }); + } + AddMenu(fsql, xuanjianDirId, "选茧日报", "/prod/xuanjian/rb", "prod/xuanjian/index", "Document", "prod:xuanjian:list", 1); + AddMenu(fsql, xuanjianDirId, "选茧计划", "/prod/xuanjian/plan", "prod/xuanjian/index", "Calendar", "prod:xuanjian:plan:list", 2); + AddMenu(fsql, xuanjianDirId, "选茧班组", "/prod/xuanjian/banzu", "prod/xuanjian/index", "User", "prod:xuanjian:banzu:list", 3); var adminRole = fsql.Select().Where(r => r.Code == "admin").First(); if (adminRole != null) diff --git a/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs b/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs index 4ed5e2e..9af5010 100644 --- a/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs +++ b/backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs @@ -23,6 +23,9 @@ public static class DbSeeder // 1. 建表(CodeFirst SyncStructure,增量同步,兼容旧库) SyncStructure(fsql); + // 1.1 仓储单据采用物理删除:清理历史软删残留,避免唯一索引阻止重建同号单据 + CleanWmsSoftDeletedRows(fsql); + // 2. 默认角色 if (!fsql.Select().Any()) { @@ -48,8 +51,8 @@ public static class DbSeeder }).ExecuteAffrows(); } - // 4. 考勤菜单(增量) - SeedAttendanceMenus(fsql); + // 4. 旧「考勤管理」模块已移除,替换为「生产-职工考勤」(CQDJ/CQTJ)。 + // 不再创建 /attendance 菜单;库中历史记录需在「系统管理-菜单管理」删除。 // 5. 业务基础数据(部门树 + 机组车台 + 庄口 + 规格 + 定级指标 + 菜单) BusinessSeedData.Seed(fsql); @@ -94,6 +97,12 @@ public static class DbSeeder // 6.695 质量模块(工艺执行书 / 样茧试缫) SeedQcMenus(fsql, snowflake); + // 6.696 薪资模块(挡车/车头产量计算 + 其依赖配置) + SeedPayRollMenus(fsql, snowflake); + + // 6.697 仓储模块(原料 / 副产品 / 在制品 / 生丝 / 物资) + SeedWmsMenus(fsql, snowflake); + // 6.71 报表中心菜单(/report 目录 + 报表中心/报表管理两个页面) SeedReportMenus(fsql, snowflake); // 6.72 旧版内嵌模板(sys_reports.template_data)迁移为独立模板实例 ReportFile @@ -174,6 +183,39 @@ public static class DbSeeder EnsureColumn(fsql, "NotificationMessages", "Category", "INTEGER NOT NULL DEFAULT 0"); EnsureColumn(fsql, "NotificationMessages", "RoutePath", "TEXT NOT NULL DEFAULT ''"); EnsureColumn(fsql, "sys_reports", "report_file_id", "INTEGER NOT NULL DEFAULT 0"); + // 落丝记录补「落丝桶数 / 每桶茧量」:生产-后缫「秤大丝」算茧量、缫折、台产需要 + EnsureColumn(fsql, "LsjlRecords", "Tongshu", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "LsjlRecords", "Meitongjianliang", "INTEGER NOT NULL DEFAULT 0"); + + // 生丝:出入库按批次/包件关联(对齐 F9 SS_Rk_Item.PiciIds / SS_Ck_Item.PcAndBjIds) + EnsureColumn(fsql, "WmsRukuDetails", "PiciIds", "TEXT NOT NULL DEFAULT ''"); + EnsureColumn(fsql, "WmsChukuDetails", "PcAndBjIds", "TEXT NOT NULL DEFAULT ''"); + // 生丝批次:出库汇总字段(对齐 F9 SS_Pici.CK_LastDate / Ck_Baoshu / Ck_Jingzhong / Kc_Baoshu / Rk_Date) + EnsureColumn(fsql, "WmsPicis", "RkDate", "TEXT NULL"); + EnsureColumn(fsql, "WmsPicis", "CkLastDate", "TEXT NULL"); + EnsureColumn(fsql, "WmsPicis", "CkBaoshu", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPicis", "CkJingzhong", "REAL NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPicis", "KcBaoshu", "INTEGER NOT NULL DEFAULT 0"); + // 生丝包件:出库标记(对齐 F9 SS_Pici_Baojian.Ck_Isck / Ck_Item_Id / Ck_date / Ck_Jingzhong) + EnsureColumn(fsql, "WmsPiciBaojians", "CkIsck", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPiciBaojians", "CkItemId", "INTEGER NOT NULL DEFAULT 0"); + EnsureColumn(fsql, "WmsPiciBaojians", "CkDate", "TEXT NULL"); + EnsureColumn(fsql, "WmsPiciBaojians", "CkJingzhong", "REAL NOT NULL DEFAULT 0"); + } + + /// 仓储单据(入库 / 出库)改用物理删除,这里清理历史软删残留行 + private static void CleanWmsSoftDeletedRows(IFreeSql fsql) + { + try + { + fsql.Ado.ExecuteNonQuery("DELETE FROM WmsRukus WHERE IsDeleted = 1;"); + fsql.Ado.ExecuteNonQuery("DELETE FROM WmsChukus WHERE IsDeleted = 1;"); + fsql.Ado.ExecuteNonQuery("DELETE FROM WmsPicis WHERE IsDeleted = 1;"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] 仓储软删残留清理跳过:{ex.Message}"); + } } /// 为已有表补加列(列已存在则忽略) @@ -731,6 +773,27 @@ public static class DbSeeder Console.WriteLine("[DbSeeder] 已清理旧权限单元 base:jizuchetai:list"); } + // 1.9) 清理旧「考勤管理」(/attendance):前端页面组件已随「生产-职工考勤」移植删除, + // 菜单留着会出现「点了打不开(空白页)」,这里按路径整体移除。 + var legacyAtt = fsql.Select().Where(m => m.Path.StartsWith("/attendance")).ToList(); + foreach (var m in legacyAtt) + { + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {m.Id}"); + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_menus WHERE id = {m.Id}"); + } + if (legacyAtt.Count > 0) + Console.WriteLine($"[DbSeeder] 已清理旧考勤菜单 {legacyAtt.Count} 条(页面组件已移除)"); + + var legacyAttPerms = fsql.Ado.ExecuteDataTable("SELECT id FROM sys_rbac_permissions WHERE code LIKE 'attendance:%'"); + foreach (DataRow permRow in legacyAttPerms.Rows) + { + var permId = Convert.ToInt64(permRow["id"]); + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_role_permissions WHERE permission_id = {permId}"); + fsql.Ado.ExecuteNonQuery($"DELETE FROM sys_rbac_permissions WHERE id = {permId}"); + } + if (legacyAttPerms.Rows.Count > 0) + Console.WriteLine($"[DbSeeder] 已清理旧考勤权限单元 {legacyAttPerms.Rows.Count} 条"); + EnsureTopMenuWithChildren(fsql, snowflake, "/prod/qianxian", "生产-前缫", "Cpu", 5, new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { @@ -738,7 +801,8 @@ public static class DbSeeder ("落丝记录", "/prod/qianxian/lsjl", "prod/lsjl/index", "prod:lsjl:list", "Document", 2), ("车间班次", "/prod/qianxian/chejianbanci", "base/chejianbanci/index", "prod:chejianbanci:list", "Timer", 3), ("看台排班", "/prod/qianxian/ktpb", "prod/ktpb/index", "prod:ktpb:list", "Grid", 4), - ("看台记录", "/prod/qianxian/ktjl", "prod/ktjl/index", "prod:ktjl:list", "Tickets", 5) + ("看台记录", "/prod/qianxian/ktjl", "prod/ktjl/index", "prod:ktjl:list", "Tickets", 5), + ("煮茧日报", "/prod/qianxian/zjrb", "prod/zjrb/index", "prod:zjrb:list", "Notebook", 6) }); // 子菜单顺序归一(EnsureTopMenuWithChildren 只补新菜单,不调整已存在菜单的排序) @@ -746,7 +810,7 @@ public static class DbSeeder { ("/prod/qianxian/jizuchetai", 1), ("/prod/qianxian/lsjl", 2), ("/prod/qianxian/chejianbanci", 3), ("/prod/qianxian/ktpb", 4), - ("/prod/qianxian/ktjl", 5) + ("/prod/qianxian/ktjl", 5), ("/prod/qianxian/zjrb", 6) }) { fsql.Update() @@ -759,9 +823,68 @@ public static class DbSeeder new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] { ("纤度检验", "/prod/houxian/xdjy", "prod/xdjy/index", "prod:xdjy:list", "EditPen", 1), - ("黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "prod:hbjy:list", "Notebook", 2) + ("黑板检验", "/prod/houxian/hbjy", "prod/hbjy/index", "prod:hbjy:list", "Notebook", 2), + ("秤大丝", "/prod/houxian/cds", "prod/cds/index", "prod:cds:list", "Box", 3), + ("公量纤度", "/prod/houxian/glxd", "prod/glxd/index", "prod:glxd:list", "TrendCharts", 4), + ("丝片检查", "/prod/houxian/spjc", "prod/spjc/index", "prod:spjc:list", "Document", 5), + ("丝片台帐", "/prod/houxian/sptz", "prod/sptz/index", "prod:sptz:list", "Files", 6) }); + // 生产-选剥茧(对齐 F9 WinClient\F9.Business.Prod\XBJ):选茧日报 / 选茧计划 / 选茧班组 + EnsureTopMenuWithChildren(fsql, snowflake, "/prod/xuanjian", "生产-选剥茧", "Grid", 7, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("选茧日报", "/prod/xuanjian/rb", "prod/xuanjian/index", "prod:xuanjian:list", "Document", 1), + ("选茧计划", "/prod/xuanjian/plan", "prod/xuanjian/index", "prod:xuanjian:plan:list", "Calendar", 2), + ("选茧班组", "/prod/xuanjian/banzu", "prod/xuanjian/index", "prod:xuanjian:banzu:list", "User", 3) + }); + + // 生产-考勤(对齐 F9 WinClient\F9.Business.Prod\KQ):考勤登记 + EnsureTopMenuWithChildren(fsql, snowflake, "/prod/kaoqin", "生产-考勤", "Clock", 8, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("考勤登记", "/prod/kaoqin/cqdj", "prod/kaoqin/index", "prod:kaoqin:cqdj:list", "EditPen", 1), + ("考勤统计", "/prod/kaoqin/cqtj", "prod/kaoqin/cqtj/index", "prod:kaoqin:cqtj:list", "DataAnalysis", 2) + }); + + // 子菜单顺序归一 + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/prod/kaoqin/cqdj", 1), ("/prod/kaoqin/cqtj", 2) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + // 子菜单顺序归一 + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/prod/xuanjian/rb", 1), ("/prod/xuanjian/plan", 2), ("/prod/xuanjian/banzu", 3) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + // 子菜单顺序归一 + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/prod/houxian/xdjy", 1), ("/prod/houxian/hbjy", 2), + ("/prod/houxian/cds", 3), ("/prod/houxian/glxd", 4), + ("/prod/houxian/spjc", 5), ("/prod/houxian/sptz", 6) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + // 3) 规范一级菜单顺序 foreach (var (path, sort) in new (string Path, int Sort)[] { @@ -791,6 +914,87 @@ public static class DbSeeder /// 质量模块菜单:一级目录「质量」+ 工艺执行书 / 样茧试缫 两个页面。 /// 模块落在「生产-后缫」之后、消息中心之前。 /// + private static void SeedPayRollMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + try + { + EnsureTopMenuWithChildren(fsql, snowflake, "/payroll", "薪资", "Money", 8, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("挡车车头产量计算", "/payroll/calcldcct", "payroll/calcldcct/index", "payroll:calcldcct:list", "Coin", 1), + ("工艺庄口薪资配置", "/payroll/confgyzk", "payroll/confgyzk/index", "payroll:confgyzk:list", "Setting", 2), + ("工种薪资配置", "/payroll/confgz", "payroll/confgz/index", "payroll:confgz:list", "Setting", 3), + ("挡车岗位薪资配置", "/payroll/confdcg", "payroll/confdcg/index", "payroll:confdcg:list", "Setting", 4), + ("挡车纤度奖扣", "/payroll/dcxdjk", "payroll/dcxdjk/index", "payroll:dcxdjk:list", "Setting", 5), + ("薪资价格配置", "/payroll/xzjg", "payroll/xzjg/index", "payroll:xzjg:list", "Setting", 6), + // 考勤统计依赖的三套配置(CONFZQ / CONFEMPLOYEE / 岗位胜任考核) + ("账期薪资配置", "/payroll/confzq", "payroll/confzq/index", "payroll:confzq:list", "Calendar", 7), + ("职工薪资配置", "/payroll/confemployee", "payroll/confemployee/index", "payroll:confemployee:list", "User", 8), + ("岗位胜任考核", "/payroll/gwsrkh", "payroll/gwsrkh/index", "payroll:gwsrkh:list", "Trophy", 9) + }); + + // 一级菜单顺序归一:质量(7) < 薪资(8) < 通知(9) < 系统(10) < 报表(11) + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/attendance", 2), ("/base", 3), ("/employee", 4), + ("/prod/qianxian", 5), ("/prod/houxian", 6), ("/qc", 7), + ("/payroll", 8), ("/notification", 9), ("/sys", 10), ("/report", 11) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + Console.WriteLine("[DbSeeder] 薪资模块菜单已就绪(挡车车头产量计算 + 五套薪资配置)"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] 薪资模块菜单种子跳过:{ex.Message}"); + } + } + + /// + /// 仓储模块菜单:一级目录「仓储」+ 五个模块页面(原料 / 副产品 / 在制品 / 生丝 / 物资)。 + /// 对齐 F9 WinClient\F9.Business.WMS 的五个子模块,落在一级菜单「薪资」之后。 + /// + private static void SeedWmsMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) + { + try + { + EnsureTopMenuWithChildren(fsql, snowflake, "/wms", "仓储", "Box", 9, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("原料", "/wms/yuanliao", "wms/yuanliao/index", "wms:yuanliao:list", "Coin", 1), + ("副产品", "/wms/fuchanpin", "wms/fuchanpin/index", "wms:fuchanpin:list", "Goods", 2), + ("中间品", "/wms/zjp", "wms/zjp/index", "wms:zjp:list", "Grid", 3), + ("生丝", "/wms/ss", "wms/ss/index", "wms:ss:list", "ShoppingCart", 4), + ("物资", "/wms/wj", "wms/wj/index", "wms:wj:list", "Tools", 5) + }); + + // 一级菜单顺序归一:质量(7) < 薪资(8) < 仓储(9) < 通知(10) < 系统(11) < 报表(12) + foreach (var (path, sort) in new (string Path, int Sort)[] + { + ("/attendance", 2), ("/base", 3), ("/employee", 4), + ("/prod/qianxian", 5), ("/prod/houxian", 6), ("/qc", 7), + ("/payroll", 8), ("/wms", 9), ("/notification", 10), ("/sys", 11), ("/report", 12) + }) + { + fsql.Update() + .Set(x => x.SortOrder, sort) + .Where(x => x.ParentId == 0 && x.Path == path && x.SortOrder != sort) + .ExecuteAffrows(); + } + + Console.WriteLine("[DbSeeder] 仓储模块菜单已就绪(原料 / 副产品 / 在制品 / 生丝 / 物资)"); + } + catch (Exception ex) + { + Console.WriteLine($"[DbSeeder] 仓储模块菜单种子跳过:{ex.Message}"); + } + } + private static void SeedQcMenus(IFreeSql fsql, ISnowflakeIdGenerator snowflake) { try @@ -862,9 +1066,10 @@ public static class DbSeeder child.SetWebInfo(path, component, permission); fsql.Insert(child).ExecuteAffrows(); } - else if (!child.IsActive || child.IconSvg != icon || child.ParentId != dir.Id) + else if (!child.IsActive || child.IconSvg != icon || child.ParentId != dir.Id || child.Name != name) { fsql.Update() + .Set(x => x.Name, name) .Set(x => x.IconSvg, icon) .Set(x => x.IsActive, true) .Set(x => x.ParentId, dir.Id) diff --git a/tools/report-designer/F9ReportDesignerHandler.ps1 b/tools/report-designer/F9ReportDesignerHandler.ps1 new file mode 100644 index 0000000..b1c8820 --- /dev/null +++ b/tools/report-designer/F9ReportDesignerHandler.ps1 @@ -0,0 +1,117 @@ +# F9 报表设计器协议宿主 +# 注册表 HKCR\f9designer\shell\open\command 指向本脚本: +# powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File "<本脚本>" "%1" +# 前端唤起:ReportDesign.vue 的 openGridDesigner() +# f9designer://open?base=&rid=<报表Id>&token=&code=<报表编号> +# +# 流程:解析协议参数 -> 下载模板 .grf -> 用锐浪 Grid++Report 7 设计器打开(阻塞等待关闭) +# -> 关闭后把模板回传后端 -> 清理临时文件 +# 日志:tools/report-designer/logs/designer_yyyyMMdd.log(排查"点了没反应"时看这里) + +param([string]$Url) + +$ErrorActionPreference = 'Stop' +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$logDir = Join-Path $scriptDir 'logs' +if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } +$log = Join-Path $logDir ('designer_{0:yyyyMMdd}.log' -f (Get-Date)) + +function Write-Log($msg) { + $line = '[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $msg + try { Add-Content -Path $log -Value $line -Encoding UTF8 } catch { } +} + +# 失败时弹窗提示(注册表里是 -WindowStyle Hidden,不弹窗的话用户只会看到"点了没反应") +function Show-Error($msg) { + Write-Log "错误: $msg" + try { + Add-Type -AssemblyName System.Windows.Forms + [System.Windows.Forms.MessageBox]::Show( + $msg + "`n`n详细日志:" + $log, 'F9 报表设计器', 'OK', 'Error') | Out-Null + } catch { } +} + +Write-Log '========== 唤起 ==========' +Write-Log "Url: $Url" + +try { + # ---------- 1. 解析协议参数 ---------- + # 关键:Windows Shell 会在协议主机后补一个 "/",实际收到的是 + # f9designer://open/?base=...&rid=... + # 所以不能用固定前缀剥离,直接取第一个 "?" 之后的全部内容。 + $qIndex = if ($Url) { $Url.IndexOf('?') } else { -1 } + $raw = if ($qIndex -ge 0) { $Url.Substring($qIndex + 1) } else { '' } + $qs = @{} + foreach ($part in ($raw -split '&')) { + if ([string]::IsNullOrWhiteSpace($part)) { continue } + $kv = $part -split '=', 2 + if ($kv.Length -eq 2) { + $key = [Uri]::UnescapeDataString($kv[0]).Trim() + $qs[$key] = [Uri]::UnescapeDataString($kv[1]) + } + } + $base = ($qs['base'] + '').TrimEnd('/') + $rid = $qs['rid'] + $token = $qs['token'] + $code = $qs['code'] + Write-Log "base=$base rid=$rid code=$code" + + if (-not $base) { + $base = 'http://localhost:5173' + Write-Log "未收到 base 参数,回退默认 $base" + } + if (-not $rid) { throw "缺少 rid 参数(Url=$Url)" } + + # ---------- 2. 定位锐浪设计器 ---------- + # 可用环境变量 F9_GR7_DESIGNER 覆盖,默认 D:\GridReport7\gr7designer64.exe + $designer = $env:F9_GR7_DESIGNER + if ([string]::IsNullOrWhiteSpace($designer)) { $designer = 'D:\GridReport7\gr7designer64.exe' } + if (-not (Test-Path $designer)) { throw "未找到锐浪设计器:$designer (可设置环境变量 F9_GR7_DESIGNER 指定)" } + Write-Log "设计器: $designer" + + # ---------- 3. 下载模板 ---------- + $tmp = Join-Path $env:TEMP ('f9_{0}_{1}.grf' -f $rid, [Guid]::NewGuid().ToString('N').Substring(0, 8)) + # 下载接口为 [AllowAnonymous],token 走 query 的 access_token + $dl = "$base/api/sys/report/$rid/template?access_token=$token" + Write-Log "下载: $dl" + try { + Invoke-WebRequest -Uri $dl -OutFile $tmp -UseBasicParsing + Write-Log ('已下载 {0} 字节' -f (Get-Item $tmp).Length) + } catch { + Write-Log "下载失败(新报表可能尚无模板),改用空白模板继续:$($_.Exception.Message)" + Set-Content -Path $tmp -Value '' -Encoding UTF8 + } + + # ---------- 4. 打开设计器并等待关闭 ---------- + Write-Log '启动设计器(等待关闭)...' + $proc = Start-Process -FilePath $designer -ArgumentList ('"{0}"' -f $tmp) -PassThru + $proc.WaitForExit() + Write-Log ('设计器已关闭,退出码 {0}' -f $proc.ExitCode) + + # ---------- 5. 回传模板 ---------- + if ((Get-Item $tmp).Length -gt 0) { + Write-Log '上传模板...' + Add-Type -AssemblyName System.Net.Http + $client = New-Object System.Net.Http.HttpClient + if ($token) { $client.DefaultRequestHeaders.TryAddWithoutValidation('Authorization', "Bearer $token") | Out-Null } + $content = New-Object System.Net.Http.MultipartFormDataContent + $fs = [System.IO.File]::OpenRead($tmp) + try { + $sc = New-Object System.Net.Http.StreamContent($fs) + $content.Add($sc, 'file', [System.IO.Path]::GetFileName($tmp)) + $resp = $client.PostAsync("$base/api/sys/report/$rid/template", $content).Result + Write-Log ('上传结果: {0}' -f $resp.StatusCode) + } finally { + $fs.Close() + $client.Dispose() + } + } else { + Write-Log '模板为空,跳过上传' + } + + Remove-Item $tmp -Force -ErrorAction SilentlyContinue + Write-Log '完成' +} catch { + Show-Error $($_.Exception.Message) + Write-Log $_.ScriptStackTrace +} diff --git a/web/src/router/index.js b/web/src/router/index.js index e9e524a..ae54711 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -21,20 +21,13 @@ export const constantRoutes = [ name: 'Dashboard', component: () => import('../views/dashboard/index.vue'), meta: { title: '首页', icon: 'HomeFilled' } - }, - // 请假审批:静态注册,所有登录用户可访问(待办消息"去处理"跳转入口,不依赖菜单权限) - { - path: 'attendance/approve', - name: 'AttendanceApprove', - component: () => import('../views/attendance/approve/index.vue'), - meta: { title: '请假审批', icon: 'Checked' } } ] } ]; // 已静态注册的路由(不随菜单动态注册,避免重复) -const STATIC_ROUTES = ['/attendance/approve']; +const STATIC_ROUTES = []; // 组件映射:后端菜单的 component 字段 -> 前端组件 export const componentMap = { @@ -43,11 +36,6 @@ export const componentMap = { 'system/role/index': () => import('../views/system/role/index.vue'), 'system/menu/index': () => import('../views/system/menu/index.vue'), 'system/log/index': () => import('../views/system/log/index.vue'), - 'attendance/punch/index': () => import('../views/attendance/punch/index.vue'), - 'attendance/my/index': () => import('../views/attendance/my/index.vue'), - 'attendance/leave/index': () => import('../views/attendance/leave/index.vue'), - 'attendance/records/index': () => import('../views/attendance/records/index.vue'), - 'attendance/approve/index': () => import('../views/attendance/approve/index.vue'), 'base/zhuangkou/index': () => import('../views/base/zhuangkou/index.vue'), 'base/guige/index': () => import('../views/base/guige/index.vue'), 'base/dingjizhibiao/index': () => import('../views/base/dingjizhibiao/index.vue'), @@ -60,6 +48,11 @@ export const componentMap = { 'base/wuzimulu/index': () => import('../views/base/wuzimulu/index.vue'), 'base/gongyingshang/index': () => import('../views/base/gongyingshang/index.vue'), 'base/employee/index': () => import('../views/base/employee/index.vue'), + 'wms/yuanliao/index': () => import('../views/wms/yuanliao/index.vue'), + 'wms/fuchanpin/index': () => import('../views/wms/fuchanpin/index.vue'), + 'wms/zjp/index': () => import('../views/wms/zjp/index.vue'), + 'wms/ss/index': () => import('../views/wms/ss/index.vue'), + 'wms/wj/index': () => import('../views/wms/wj/index.vue'), 'qc/gyzxs/index': () => import('../views/qc/gyzxs/index.vue'), 'qc/yjss/index': () => import('../views/qc/yjss/index.vue'), 'prod/lsjl/index': () => import('../views/prod/lsjl/index.vue'), @@ -67,6 +60,26 @@ export const componentMap = { 'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'), 'prod/ktpb/index': () => import('../views/prod/ktpb/index.vue'), 'prod/ktjl/index': () => import('../views/prod/ktjl/index.vue'), + 'prod/zjrb/index': () => import('../views/prod/zjrb/index.vue'), + 'prod/cds/index': () => import('../views/prod/cds/index.vue'), + 'prod/glxd/index': () => import('../views/prod/glxd/index.vue'), + 'prod/spjc/index': () => import('../views/prod/spjc/index.vue'), + // 生产-选剥茧:选茧日报 / 选茧计划 / 选茧班组(对齐 F9 WinClient\F9.Business.Prod\XBJ) + 'prod/xuanjian/index': () => import('../views/prod/xuanjian/index.vue'), + // 生产-考勤:考勤登记 / 考勤统计(对齐 F9 WinClient\F9.Business.Prod\KQ\CQDJ / CQTJ) + 'prod/kaoqin/index': () => import('../views/prod/kaoqin/index.vue'), + 'prod/kaoqin/cqtj/index': () => import('../views/prod/kaoqin/cqtj/index.vue'), + // 考勤统计依赖的薪资配置(CONFZQ / CONFEMPLOYEE / 岗位胜任考核) + 'payroll/confzq/index': () => import('../views/payroll/confzq/index.vue'), + 'payroll/confemployee/index': () => import('../views/payroll/confemployee/index.vue'), + 'payroll/gwsrkh/index': () => import('../views/payroll/gwsrkh/index.vue'), + 'prod/sptz/index': () => import('../views/prod/sptz/index.vue'), + 'payroll/calcldcct/index': () => import('../views/payroll/calcldcct/index.vue'), + 'payroll/confgyzk/index': () => import('../views/payroll/confgyzk/index.vue'), + 'payroll/confgz/index': () => import('../views/payroll/confgz/index.vue'), + 'payroll/confdcg/index': () => import('../views/payroll/confdcg/index.vue'), + 'payroll/dcxdjk/index': () => import('../views/payroll/dcxdjk/index.vue'), + 'payroll/xzjg/index': () => import('../views/payroll/xzjg/index.vue'), 'notification/messages/index': () => import('../views/notification/messages/index.vue'), 'notification/subscribe/index': () => import('../views/notification/subscribe/index.vue'), 'notification/type/index': () => import('../views/notification/type/index.vue'), diff --git a/web/src/views/attendance/approve/index.vue b/web/src/views/attendance/approve/index.vue deleted file mode 100644 index c698ec6..0000000 --- a/web/src/views/attendance/approve/index.vue +++ /dev/null @@ -1,154 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/leave/index.vue b/web/src/views/attendance/leave/index.vue deleted file mode 100644 index c4b7ab4..0000000 --- a/web/src/views/attendance/leave/index.vue +++ /dev/null @@ -1,141 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/my/index.vue b/web/src/views/attendance/my/index.vue deleted file mode 100644 index dab1a2c..0000000 --- a/web/src/views/attendance/my/index.vue +++ /dev/null @@ -1,160 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/punch/index.vue b/web/src/views/attendance/punch/index.vue deleted file mode 100644 index 653d015..0000000 --- a/web/src/views/attendance/punch/index.vue +++ /dev/null @@ -1,207 +0,0 @@ - - - - - diff --git a/web/src/views/attendance/records/index.vue b/web/src/views/attendance/records/index.vue deleted file mode 100644 index 58fdf7d..0000000 --- a/web/src/views/attendance/records/index.vue +++ /dev/null @@ -1,133 +0,0 @@ - - - - - diff --git a/web/src/views/dashboard/index.vue b/web/src/views/dashboard/index.vue index 71c3632..163d419 100644 --- a/web/src/views/dashboard/index.vue +++ b/web/src/views/dashboard/index.vue @@ -10,7 +10,6 @@ const quickLinks = [ { title: '落丝记录', desc: '前缫 · 落丝管理', icon: 'DataLine', path: '/prod/qianxian/lsjl', color: '#1e6b52' }, { title: '纤度检验', desc: '后缫 · 纤度 / 野纤等级判定', icon: 'ScaleToOriginal', path: '/prod/houxian/xdjy', color: '#c9a54a' }, { title: '黑板检验', desc: '后缫 · 清洁 / 洁净等级', icon: 'Grid', path: '/prod/houxian/hbjy', color: '#3f8f6e' }, - { title: '考勤打卡', desc: '上下班签到签退', icon: 'AlarmClock', path: '/attendance/punch', color: '#b8873a' } ]; function go(path) { router.push(path); } diff --git a/web/src/views/payroll/calcldcct/index.vue b/web/src/views/payroll/calcldcct/index.vue new file mode 100644 index 0000000..18637a6 --- /dev/null +++ b/web/src/views/payroll/calcldcct/index.vue @@ -0,0 +1,299 @@ + + + + + diff --git a/web/src/views/payroll/confdcg/index.vue b/web/src/views/payroll/confdcg/index.vue new file mode 100644 index 0000000..e87ab8b --- /dev/null +++ b/web/src/views/payroll/confdcg/index.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/web/src/views/payroll/confemployee/index.vue b/web/src/views/payroll/confemployee/index.vue new file mode 100644 index 0000000..5176394 --- /dev/null +++ b/web/src/views/payroll/confemployee/index.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/web/src/views/payroll/confgyzk/index.vue b/web/src/views/payroll/confgyzk/index.vue new file mode 100644 index 0000000..b1f7368 --- /dev/null +++ b/web/src/views/payroll/confgyzk/index.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/web/src/views/payroll/confgz/index.vue b/web/src/views/payroll/confgz/index.vue new file mode 100644 index 0000000..103871c --- /dev/null +++ b/web/src/views/payroll/confgz/index.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/web/src/views/payroll/confzq/index.vue b/web/src/views/payroll/confzq/index.vue new file mode 100644 index 0000000..489589f --- /dev/null +++ b/web/src/views/payroll/confzq/index.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/web/src/views/payroll/dcxdjk/index.vue b/web/src/views/payroll/dcxdjk/index.vue new file mode 100644 index 0000000..30a2d0b --- /dev/null +++ b/web/src/views/payroll/dcxdjk/index.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/web/src/views/payroll/gwsrkh/index.vue b/web/src/views/payroll/gwsrkh/index.vue new file mode 100644 index 0000000..0d855e3 --- /dev/null +++ b/web/src/views/payroll/gwsrkh/index.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/web/src/views/payroll/xzjg/index.vue b/web/src/views/payroll/xzjg/index.vue new file mode 100644 index 0000000..886d7d6 --- /dev/null +++ b/web/src/views/payroll/xzjg/index.vue @@ -0,0 +1,123 @@ + + + + + diff --git a/web/src/views/prod/cds/index.vue b/web/src/views/prod/cds/index.vue new file mode 100644 index 0000000..bbebfeb --- /dev/null +++ b/web/src/views/prod/cds/index.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/web/src/views/prod/glxd/index.vue b/web/src/views/prod/glxd/index.vue new file mode 100644 index 0000000..9a09cf4 --- /dev/null +++ b/web/src/views/prod/glxd/index.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/src/views/prod/kaoqin/cqtj/index.vue b/web/src/views/prod/kaoqin/cqtj/index.vue new file mode 100644 index 0000000..00f0d0d --- /dev/null +++ b/web/src/views/prod/kaoqin/cqtj/index.vue @@ -0,0 +1,312 @@ + + + + + diff --git a/web/src/views/prod/kaoqin/index.vue b/web/src/views/prod/kaoqin/index.vue new file mode 100644 index 0000000..377bd7c --- /dev/null +++ b/web/src/views/prod/kaoqin/index.vue @@ -0,0 +1,608 @@ + + + + + diff --git a/web/src/views/prod/lsjl/index.vue b/web/src/views/prod/lsjl/index.vue index cfc38d7..58453c3 100644 --- a/web/src/views/prod/lsjl/index.vue +++ b/web/src/views/prod/lsjl/index.vue @@ -28,7 +28,8 @@ const editId = ref(null); const form = reactive({ workDate: new Date(), deptId: null, gyzkId: null, jizu: 1, luosiTime: '', yanse: '', huishu: 0, chetaiStart: 0, chetaiEnd: 0, - chetaishu: 0, isbanche: false, hours: 0, guige: '', chesu: 0 + chetaishu: 0, isbanche: false, hours: 0, tongshu: 0, meitongjianliang: 0, + guige: '', chesu: 0 }); async function load() { @@ -86,7 +87,8 @@ function openCreate() { Object.assign(form, { workDate: new Date(), deptId: deptOptions.value[0]?.id || null, gyzkId: null, jizu: 1, luosiTime: '', yanse: yanseOptions[0] || '', huishu: 0, chetaiStart: 0, chetaiEnd: 0, - chetaishu: 0, isbanche: false, hours: 0, guige: guigeOptions.value[0] || '', chesu: 0 + chetaishu: 0, isbanche: false, hours: 0, tongshu: 0, meitongjianliang: 0, + guige: guigeOptions.value[0] || '', chesu: 0 }); zkName.value = ''; dialogVisible.value = true; @@ -100,7 +102,9 @@ function openEdit(row) { workDate: row.workDate, deptId: row.deptId, gyzkId: row.gyzkId, jizu: row.jizu, luosiTime: row.luosiTime, yanse: row.yanse, huishu: row.huishu, chetaiStart: row.chetaiStart, chetaiEnd: row.chetaiEnd, chetaishu: row.chetaishu, - isbanche: row.isbanche, hours: row.hours, guige: row.guige, chesu: row.chesu + isbanche: row.isbanche, hours: row.hours, + tongshu: row.tongshu || 0, meitongjianliang: row.meitongjianliang || 0, + guige: row.guige, chesu: row.chesu }); zkName.value = row.zhuangKouName || ''; dialogVisible.value = true; @@ -249,6 +253,18 @@ onMounted(() => { load(); loadDepts(); loadGuige(); }); + + + + + + + + + + + + diff --git a/web/src/views/prod/spjc/index.vue b/web/src/views/prod/spjc/index.vue new file mode 100644 index 0000000..a80b613 --- /dev/null +++ b/web/src/views/prod/spjc/index.vue @@ -0,0 +1,287 @@ + + + + + diff --git a/web/src/views/prod/sptz/index.vue b/web/src/views/prod/sptz/index.vue new file mode 100644 index 0000000..de7b7c6 --- /dev/null +++ b/web/src/views/prod/sptz/index.vue @@ -0,0 +1,272 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/XjpbBanzuPanel.vue b/web/src/views/prod/xuanjian/XjpbBanzuPanel.vue new file mode 100644 index 0000000..b58790a --- /dev/null +++ b/web/src/views/prod/xuanjian/XjpbBanzuPanel.vue @@ -0,0 +1,208 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/XjrbPanel.vue b/web/src/views/prod/xuanjian/XjrbPanel.vue new file mode 100644 index 0000000..4523cac --- /dev/null +++ b/web/src/views/prod/xuanjian/XjrbPanel.vue @@ -0,0 +1,390 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/XjrbPlanPanel.vue b/web/src/views/prod/xuanjian/XjrbPlanPanel.vue new file mode 100644 index 0000000..9052f5b --- /dev/null +++ b/web/src/views/prod/xuanjian/XjrbPlanPanel.vue @@ -0,0 +1,404 @@ + + + + + diff --git a/web/src/views/prod/xuanjian/index.vue b/web/src/views/prod/xuanjian/index.vue new file mode 100644 index 0000000..0b945dc --- /dev/null +++ b/web/src/views/prod/xuanjian/index.vue @@ -0,0 +1,30 @@ + + + diff --git a/web/src/views/prod/zjrb/ZjrbEditDialog.vue b/web/src/views/prod/zjrb/ZjrbEditDialog.vue new file mode 100644 index 0000000..77ef53c --- /dev/null +++ b/web/src/views/prod/zjrb/ZjrbEditDialog.vue @@ -0,0 +1,531 @@ + + + + + diff --git a/web/src/views/prod/zjrb/index.vue b/web/src/views/prod/zjrb/index.vue new file mode 100644 index 0000000..ef50776 --- /dev/null +++ b/web/src/views/prod/zjrb/index.vue @@ -0,0 +1,354 @@ + + + + + diff --git a/web/src/views/wms/components/ChukuPanel.vue b/web/src/views/wms/components/ChukuPanel.vue new file mode 100644 index 0000000..3b3763c --- /dev/null +++ b/web/src/views/wms/components/ChukuPanel.vue @@ -0,0 +1,715 @@ + + + + + diff --git a/web/src/views/wms/components/PiciSelectDialog.vue b/web/src/views/wms/components/PiciSelectDialog.vue new file mode 100644 index 0000000..83ac940 --- /dev/null +++ b/web/src/views/wms/components/PiciSelectDialog.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/web/src/views/wms/components/RukuPanel.vue b/web/src/views/wms/components/RukuPanel.vue new file mode 100644 index 0000000..77dd37b --- /dev/null +++ b/web/src/views/wms/components/RukuPanel.vue @@ -0,0 +1,722 @@ + + + + + diff --git a/web/src/views/wms/components/SsPici.vue b/web/src/views/wms/components/SsPici.vue new file mode 100644 index 0000000..003424d --- /dev/null +++ b/web/src/views/wms/components/SsPici.vue @@ -0,0 +1,528 @@ + + + + + diff --git a/web/src/views/wms/components/TaizhangPanel.vue b/web/src/views/wms/components/TaizhangPanel.vue new file mode 100644 index 0000000..dbe87cb --- /dev/null +++ b/web/src/views/wms/components/TaizhangPanel.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/web/src/views/wms/components/WjCgbx.vue b/web/src/views/wms/components/WjCgbx.vue new file mode 100644 index 0000000..83927c9 --- /dev/null +++ b/web/src/views/wms/components/WjCgbx.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/web/src/views/wms/components/WjCgplan.vue b/web/src/views/wms/components/WjCgplan.vue new file mode 100644 index 0000000..b0ef3c9 --- /dev/null +++ b/web/src/views/wms/components/WjCgplan.vue @@ -0,0 +1,240 @@ + + + + + diff --git a/web/src/views/wms/components/WjCrk.vue b/web/src/views/wms/components/WjCrk.vue new file mode 100644 index 0000000..50f9fb3 --- /dev/null +++ b/web/src/views/wms/components/WjCrk.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/web/src/views/wms/components/WjKucun.vue b/web/src/views/wms/components/WjKucun.vue new file mode 100644 index 0000000..3cd2aef --- /dev/null +++ b/web/src/views/wms/components/WjKucun.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/web/src/views/wms/components/YlCaigou.vue b/web/src/views/wms/components/YlCaigou.vue new file mode 100644 index 0000000..a108332 --- /dev/null +++ b/web/src/views/wms/components/YlCaigou.vue @@ -0,0 +1,191 @@ + + + + + diff --git a/web/src/views/wms/components/YlFbplan.vue b/web/src/views/wms/components/YlFbplan.vue new file mode 100644 index 0000000..78cb53d --- /dev/null +++ b/web/src/views/wms/components/YlFbplan.vue @@ -0,0 +1,262 @@ + + + + + diff --git a/web/src/views/wms/fuchanpin/index.vue b/web/src/views/wms/fuchanpin/index.vue new file mode 100644 index 0000000..d9c59a2 --- /dev/null +++ b/web/src/views/wms/fuchanpin/index.vue @@ -0,0 +1,44 @@ + + + + + diff --git a/web/src/views/wms/moduleConfig.js b/web/src/views/wms/moduleConfig.js new file mode 100644 index 0000000..16613e5 --- /dev/null +++ b/web/src/views/wms/moduleConfig.js @@ -0,0 +1,153 @@ +// 仓储(WMS)五个模块的差异化配置:对齐 F9 WinClient\F9.Business.WMS +// module: 1原料 2副产品 3中间品 4生丝 5物资 + +export const WMS_MODULES = { + 1: { + name: '原料', + detailItem: 'ylzk', // 出入库明细按「原料庄口」 + tzItem: 'ylzk', // 库存台账按「原料庄口」 + valueMode: 'weight', // 件数 / 净重 + showSunhao: true, // 运损 + showBangmadan: true, // 磅码单 + showYangjian: true, // 样茧件数(台账) + ruku: { gys: true, lianxiren: true, phone: true, carno: true, kuguan: true }, + chuku: { gyzk: true, kh: true, kuguan: true }, + extras: ['caigou', 'fbplan'] + }, + 2: { + name: '副产品', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'weight', + ruku: { kh: true, gyzk: true, dep: true, kuguan: true }, + chuku: { kh: true, dbdep: true, kuguan: true }, + extras: [] + }, + 3: { + name: '中间品', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'weight', + showBiaobao: true, // 标包重量 / 包装重量 + showGyzkInDetail: true, + showDepInDetail: true, + showBanci: true, // 班次 + ruku: { dep: true, kuguan: true }, + chuku: { kh: true, banci: true, kuguan: true }, + extras: ['ltj'] // 笼筒架台账 + }, + 4: { + name: '生丝', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'weight', + showPici: true, // 批次号 / 批数 + showJidiWaigou: true, // 基地 / 外购 + jianshuLabel: '批数', + ruku: { kuguan: true }, + chuku: { kh: true, kuguan: true }, + extras: ['pici'] // 批次包件 + }, + 5: { + name: '物资', + detailItem: 'wuzi', + tzItem: 'wuzi', + valueMode: 'amount', // 数量 / 金额 + showAmount: true, + ruku: { gys: true, caigouyuan: true, pay: true, rkStatus: true, kuguan: true }, + chuku: { lldep: true, usedep: true, dbdep: true, lingliaoren: true, useLocation: true, ckStatus: true, kuguan: true }, + extras: ['kucun', 'crk', 'cgplan', 'cgbx'] + } +}; + +export function moduleMeta(module) { + return WMS_MODULES[Number(module)] || WMS_MODULES[5]; +} + +// 台账两列表头 +export function valueLabels(module) { + const m = moduleMeta(module); + return m.valueMode === 'amount' + ? { jianshu: '数量', jingzhong: '金额', yingkui: '金额盈亏', unit2: '元' } + : { jianshu: m.jianshuLabel || '件数', jingzhong: '重量', yingkui: '重量盈亏', unit2: 'kg' }; +} + +export function itemLabel(module, kind = 0) { + const m = moduleMeta(module); + if (kind === 1) return '工艺庄口'; + return m.tzItem === 'ylzk' ? '原料庄口' : '物资名称'; +} + +export const KC_STATUS_OPTIONS = [ + { value: 0, label: '全部' }, + { value: 1, label: '待入库' }, + { value: 2, label: '已入库' }, + { value: 3, label: '部分出库' }, + { value: 4, label: '已出库' } +]; + +export const FB_TYPE_OPTIONS = [ + { value: 1, label: '翻包中' }, + { value: 2, label: '完成' }, + { value: 3, label: '暂停' } +]; + +export const RUKU_STATUS_OPTIONS = [ + { value: 1, label: '有效' }, + { value: 2, label: '作废' }, + { value: 3, label: '无' } +]; + +export const CGPLAN_STATUS_OPTIONS = [ + { value: 0, label: '草稿' }, + { value: 1, label: '已确认' }, + { value: 2, label: '已完成' } +]; + +export function fmt2(v) { + const n = Number(v || 0); + return n.toFixed(2); +} + +// 提交前清洗:下拉清空后为 null,而后端数值型(long / int / decimal)不接受 null, +// 这里统一把 null / undefined 转成 0(日期型保持 null)。 +const KEEP_NULL_KEYS = ['scDate', 'guobangShijian', 'ckTime', 'hongchongId']; +const NUMERIC_KEYS = [ + 'id', 'jianshu', 'maozhong', 'pizhong', 'jingzhong', 'biaobaoZhongliang', 'baozhuangZhongliang', + 'shuliang', 'danjia', 'jine', 'sunhao', 'kucunNum', 'piciNum', 'baojianNum', 'baoshu', + 'startBaojianNum', 'huichaolv', 'gongliang', 'yingkui', 'yangjianjianshu', + 'qingjie', 'jiejing', 'piancha', 'zongcha', 'qieduan', + 'type', 'rkStatus', 'bxStatus', 'ckStatus', 'status', 'fbType', 'rukuStatus', 'codeType', 'kcStatus', 'kind' +]; + +export function cleanPayload(obj) { + if (obj === null || obj === undefined) return 0; + if (Array.isArray(obj)) return obj.map((x) => (x && typeof x === 'object' ? cleanPayload(x) : x)); + if (typeof obj !== 'object' || obj instanceof Date) return obj; + + const out = {}; + Object.keys(obj).forEach((k) => { + const v = obj[k]; + if (v === null || v === undefined) { + if (KEEP_NULL_KEYS.includes(k)) out[k] = null; + else if (/Id$/.test(k) || NUMERIC_KEYS.includes(k)) out[k] = 0; + else out[k] = ''; + return; + } + if (Array.isArray(v)) { + out[k] = v.map((x) => (x && typeof x === 'object' ? cleanPayload(x) : x)); + return; + } + if (typeof v === 'object' && !(v instanceof Date)) { + out[k] = cleanPayload(v); + return; + } + out[k] = v; + }); + return out; +} + +export function today() { + const d = new Date(); + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} diff --git a/web/src/views/wms/ss/index.vue b/web/src/views/wms/ss/index.vue new file mode 100644 index 0000000..27ccfbe --- /dev/null +++ b/web/src/views/wms/ss/index.vue @@ -0,0 +1,46 @@ + + + + + diff --git a/web/src/views/wms/wj/index.vue b/web/src/views/wms/wj/index.vue new file mode 100644 index 0000000..2953868 --- /dev/null +++ b/web/src/views/wms/wj/index.vue @@ -0,0 +1,52 @@ + + + + + diff --git a/web/src/views/wms/yuanliao/index.vue b/web/src/views/wms/yuanliao/index.vue new file mode 100644 index 0000000..bfb33d6 --- /dev/null +++ b/web/src/views/wms/yuanliao/index.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/web/src/views/wms/zjp/index.vue b/web/src/views/wms/zjp/index.vue new file mode 100644 index 0000000..c6b3d64 --- /dev/null +++ b/web/src/views/wms/zjp/index.vue @@ -0,0 +1,45 @@ + + + + + diff --git a/工作进度/工作进度-14.md b/工作进度/工作进度-14.md new file mode 100644 index 0000000..d442154 --- /dev/null +++ b/工作进度/工作进度-14.md @@ -0,0 +1,143 @@ +# 工作进度-14:生产-前缫「煮茧日报」落地 + +> 时间:2026-10-22(承接工作进度-13) +> 需求:把 `D:\AntaiProject - 副本` 生产-前缫的「煮茧日报(ZJRB)」移植进本系统。 +> 参考实现:`WinClient\F9.Business.Prod\QS\ZJRB\`(Page_Zjrb、Page_Zjrb_EditV2)、 +> `Application\CQRS\Business\PROD\QS\ZJRB\`(Zjrb_SaveCommand / Zjrb_DelCommand / CalculateJizuSaosiHoursCommand)、 +> `Domain\Entities\Business\PROD\QS\`(Zjrb / ZjrbJizu / Zjrb_Banci / ZjrbJizuBanci)。 + +--- + +## 一、功能设计 + +``` +生产-前缫 +├── 机组车台 +├── 落丝记录 +├── 车间班次 +├── 看台排班 +├── 看台记录 +└── 煮茧日报(本次新增) /prod/qianxian/zjrb + 过滤:账期(月份)+ 车间(前缫)+ 庄口/规格关键字 + 主表:日期、上日、车间、庄口、规格;上存/领入/今缫/退回/今存(包数 + 重量,多级表头) + 选中行 → 下方联动展示该日报的「机组」桶数明细(含合计行) + 按钮:删除、新增、修改、计算缫丝时长 +``` + +| F9 行为 | 本系统实现 | +|---|---| +| 列表按「账期(月份)+ 车间 + 关键字」查询,无分页 | 同口径,另加后端分页(`pageIndex/pageSize`) | +| 上表主表 + 下表机组联动(点击行刷新机组) | 同左(主表 `row-click` → `GET /{id}/jizus`) | +| `删除`:四张表级联删除 | `DELETE /api/prod/zjrb/{id}`(主表 + 班次 + 机组 + 机组班次物理删除) | +| `新增`:按上一日结存带出上存、每桶茧量、工艺落丝桶数 | `GET /defaults?date=&depId=&gyzkId=&guige=` | +| `修改`:回填主表 + 班次 + 机组 + 机组班次 | `GET /{id}` | +| `保存`:主表 + 三层明细一起提交 | `POST /save`(主表 upsert + 三层明细先删后建) | +| `计算缫丝时长`:按「日期 + 车间」整日重算 | `POST /calculate-saosi-hours?date=&depId=` | + +**计算口径**(移植 `Page_Zjrb_EditV2` 的 `CalItem` / `Cal_Zhibiao` / `Cal_Zhibiao_Main`,前后端同口径、服务端保存时重算): + +``` +机组.今缫桶数 = 甲班今缫 + 乙班今缫 +机组.今存桶数 = 上存 + 今缫 − 今落 + (今落为 0 且余量 ≥ 工艺落丝桶数时,自动把今落置为工艺落丝桶数再扣减) +班次.今缫重量 = 该班次机组今缫桶数 × 每桶茧量 ÷ 1000 (每桶茧量单位 g,重量 kg) +班次.领入/退回重量 = 包数 × 联统茧标重 +班次.今缫包数 = 今缫重量 ÷ 联统茧标重(四舍五入取整) +班次.今存 = 上存 + 领入 − 今缫 − 退回 +主表 = 甲班 + 乙班(包数/重量各 5 项) +``` + +**校验**(对齐 F9 `AddValidRules`,前端拦截 + 服务端兜底):每桶茧量 > 0、工艺落丝桶数 > 0、上存包数 > 0 时上存重量必须 > 0、机组今缫桶数 > 0 时缫丝时长必须 > 0、机组缫丝时长 ≤ 20、同「日期 + 车间 + 庄口 + 规格」唯一。 + +--- + +## 二、后端 + +### 2.1 实体(`Domain/Entities/Prod/Zjrb.cs`) + +| 类 | 表 | 主要字段 | +|---|---|---| +| `ZjrbRecord` | `ZjrbRecords` | Date、DeptId、GyzkId、Guige、LastDate、StandardLuositongshu、MeitongJianliang + 包数/重量各 5 项 | +| `ZjrbJizu` | `ZjrbJizus` | ZjrbId、Jizu、JizuSaosiHours、TongshuShangcun/Jinsao/Jinluo/Jincun | +| `ZjrbBanci` | `ZjrbBancis` | ZjrbId、Banci(甲/乙)、包数/重量各 5 项 | +| `ZjrbJizuBanci` | `ZjrbJizuBancis` | ZjrbId、ZjrbJizuId、Jizu、Banci、TongshuJinsao | + +> 均继承 `BaseEntity`(自增主键 + 软删除),表结构由启动时 `CodeFirst.SyncStructure` 自动创建,无需迁移。 +> F9 的 `Zjrb_Banci.BanciId`(指向车间班次)在本系统直接落 `Prod_Banci_Enum`(甲=1/乙=2),与看台排班/看台记录一致。 + +### 2.2 服务与接口(`ZjrbService` / `ZjrbController`,路由 `api/prod/zjrb`) + +``` +GET /api/prod/zjrb/list?dateFrom=&dateTo=&depId=&like=&pageIndex=&pageSize= +GET /api/prod/zjrb/{id} 修改回填(主表 + 班次 + 机组) +GET /api/prod/zjrb/{id}/jizus 机组明细(列表页下半表) +GET /api/prod/zjrb/defaults?date=&depId=&gyzkId=&guige= 新增默认值 +POST /api/prod/zjrb/save 整条保存 +DELETE /api/prod/zjrb/{id} 级联删除 +POST /api/prod/zjrb/calculate-saosi-hours?date=&depId= 计算缫丝时长 +``` + +- 新增默认值来源:同「车间 + 庄口(+ 规格)」下 `Date < 目标日期` 的最近一条日报 + → 上存包数/重量 = 上一日今存;每桶茧量、工艺落丝桶数、`LastDate` 一并继承; + 班次行按「车间班次」配置(未配置则甲乙两班)生成,机组行按「机组车台」生成; +- 联统茧标重取参数配置 `生产\前缫\煮茧 → 联统茧标重`(本库已配置为 18),随默认值/回填一并下发,弹窗内可临时调整; +- `CalculateJizuSaosiHours` 算法(对齐 F9):机组「今缫桶数 > 0」的班次数为 2 → 18 小时,否则 9 小时; + 按「庄口 + 规格 + 机组」分组,以 `Σ(每桶茧量 × 机组今缫桶数)` 为权重把机组总时长按比例分配到各条日报的机组行。 + +### 2.3 菜单 + +`DbSeeder.SeedProdMenus` 生产-前缫子菜单新增第 6 项,并做排序归一: + +| 顺序 | 菜单 | 路径 | 组件 | 权限标识 | +|---|---|---|---|---| +| 6 | 煮茧日报 | /prod/qianxian/zjrb | prod/zjrb/index | prod:zjrb:list | + +启动日志:`[DbSeeder] 生产模块菜单已就绪(生产-前缫 / 生产-后缫)`,`/auth/menus` 已返回 6 个子菜单。 + +--- + +## 三、前端 + +| 文件 | 说明 | +|---|---| +| `views/prod/zjrb/index.vue` | 日报列表(账期月份 + 车间 + 关键字)、主表多级表头、选中行联动机组明细、合计行、删除/新增/修改/计算缫丝时长 | +| `views/prod/zjrb/ZjrbEditDialog.vue` | 录入弹窗:基础信息 + 班次网格(甲乙班 5 组包数重量)+ 机组网格(时长/上存/甲乙班今缫/今落),改动即时重算 | +| `router/index.js` | `componentMap` 注册 `prod/zjrb/index` | + +交互要点: +- 新增时选「工艺庄口 / 规格」后自动带出上一日结存与机组行,并用 `ElMessage` 提示「未维护机组车台」等异常; +- 弹窗内今缫重量/今缫包数/今存/今缫桶数/今存桶数均为只读派生值,避免与口径冲突; +- 删除与计算缫丝时长均二次确认(计算会覆盖该日该车间全部机组时长)。 + +--- + +## 四、验证记录 + +| 项 | 结果 | +|---|---| +| `dotnet build`(Domain/Application/Infrastructure) | ✅ 0 错误 | +| `dotnet build`(Api,临时输出目录,规避运行中进程文件锁) | ✅ 0 错误 | +| `vite build` | ✅ 通过(仅既有体积/SignalR 注释警告) | +| 菜单 | ✅ 生产-前缫 6 个子菜单,煮茧日报排第 6 | +| 保存(甲乙班 + 2 机组,每桶茧量 600g、标重 18) | ✅ 主表今缫 = round(54/18) = 3 包 / 54 kg;主表五组值 = 甲乙相加 | +| 机组今存自动补落丝 | ✅ 机组 上存 50 + 今缫 60 = 110 < 170(工艺落丝桶数)→ 今落 0、今存 110 | +| 计算缫丝时长 | ✅ 机组1(单班)9 h、机组2(甲乙班均有今缫)18 h,提示「已更新 1 条机组缫丝时长」 | +| 新增默认值 | ✅ 次日带出:上日 2026-10-22、上存 28 包 / 1686 kg、机组上存 110 / 110、每桶茧量 600、落丝桶数 170、标重 18 | +| 唯一性 | ✅ 同「日期 + 车间 + 庄口 + 规格」再存返回 400「已存在相同的煮茧日报!」 | +| 修改 | ✅ 甲班领入 20 → 30 包后:今存 36 + 14 = 50 包 / 1068 + 456 = 1524 kg | +| 删除 | ✅ 删除后列表为该日 0 条、回读返回「煮茧日报不存在或已删除」 | +| 删除后重建无残留 | ✅ 机组桶数 90 / 130 / 0 / 220 未翻倍(三层明细先删后建生效) | +| 测试数据清理 | ✅ 验证用日报已全部删除,未在库中留下业务数据 | + +> 验证方式:用 `-p:BaseOutputPath=obj\verify\` 另编一份 Api,在 3001 端口以真实库启动(`ConnectionStrings__Default` + `Kestrel__Endpoints__Http__Url` 覆盖),验证完已停止该实例。 +> 3000 端口的后端进程已重启(旧进程无 `api/prod/zjrb` 控制器,首次进入菜单会 404 并提示「请求失败,请稍后重试」);重启后 `GET /api/prod/zjrb/list` 返回 200,`/auth/menus` 生产-前缫为 6 项。 + +--- + +## 五、说明与后续 + +1. **未导入历史数据**:本次没有随附煮茧日报的导出数据(xlsx/csv),故未做 `ProdSeedData` 扩展;如需把 F9 的 `biz_prod_qs_zjrb*` 四张表导入,可按工作进度-13 的 CSV 内嵌 + 幂等策略追加(主表按「日期+车间+庄口+规格」、机组按「日期+机组」、班次按「日期+班次」)。 +2. **保存语义更严格**:F9 的 `Zjrb_SaveCommand` 是逐行 upsert(前端删行不会落库),本系统为「整条替换」(三层明细先删后建),行被移除时会真正删除。 +3. **机组行不再过滤全 0 行**:与 F9 现用保存命令一致(旧 `CreateZjrbCommand` 会过滤三桶数全 0 的机组行)。 +4. **联统茧标重**:读取参数配置 `生产\前缫\煮茧 → 联统茧标重`(当前 18);未配置时重量换算按录入值保留,不会清零。 +5. **待接入**:① 计件工资(F9 `CalcLdcct` 用「机组班次今缫桶数 × 每桶茧量」)已具备数据基础,可在薪酬模块直接取用;② 落丝记录联动(F9 用煮茧的每桶茧量/机组落丝桶数回填落丝记录)——本系统落丝记录暂无桶数/每桶茧量字段,需先扩展;③ 报表「煮茧日报月报 / 机组缫丝时长 / 联统茧盈亏」可基于 `ZjrbRecords + ZjrbJizus` 直接做数据源。 diff --git a/工作进度/工作进度-15.md b/工作进度/工作进度-15.md new file mode 100644 index 0000000..7bc98bf --- /dev/null +++ b/工作进度/工作进度-15.md @@ -0,0 +1,162 @@ +# 工作进度-15:生产-后缫完善(秤大丝 / 公量纤度 / 丝片检查 / 丝片台帐) + +> 时间:2026-10-24(承接工作进度-14) +> 需求:把 `D:\AntaiProject - 副本` 生产-后缫(`WinClient\F9.Business.Prod\HS\`)尚未落地的模块移植进本系统。 +> 参考实现:`WinClient\F9.Business.Prod\HS\`(CDS / GLXD / SPJC / SPTZ)、 +> `Application\CQRS\Business\PROD\HS\`(Cds_SaveCommand / Cds_Lsjl_CalZhibiaoCommand / Glxd_Update / Spjc_*/Sptz_*)、 +> `Domain\Entities\Business\PROD\HS\`(Cds_Chetai / Cds_Lsjl / Glxd / Spjc / Sptz)。 +> 移植后本系统「生产-后缫」子菜单由 2 项补齐到 6 项。 + +--- + +## 一、功能设计 + +``` +生产-后缫 +├── 纤度检验 (已有) +├── 黑板检验 (已有) +├── 秤大丝 /prod/houxian/cds 本次新增 +├── 公量纤度 /prod/houxian/glxd 本次新增 +├── 丝片检查 /prod/houxian/spjc 本次新增 +└── 丝片台帐 /prod/houxian/sptz 本次新增 +``` + +| F9 行为 | 本系统实现 | +|---|---| +| **秤大丝**:列表按「日期 + 车间」列出落丝记录,行 → 弹窗「秤大丝」,按车台录入丝量1~4 | `GET /api/prod/cds/list`、`GET /{lsjlId}`、`POST /save`、`DELETE /{lsjlId}` | +| 汇总 = Σ个数 / Σ丝量,并算茧量、缫折、台产(`Cds_Lsjl_CalZhibiaoCommand`) | 服务端保存时重算,列表直接展示 | +| **公量纤度**:按「日期 + 车间」整日批量录入,左侧带出纤度检验的平均纤度 | `GET /api/prod/glxd/list`、`POST /save`(整日提交) | +| **丝片检查**:按「日期 + 车间 + 工艺庄口 + 规格」记录前缫 7 项 / 后缫 12 项疵点片数 | `GET /api/prod/spjc/list`、`GET /{id}`、`POST /save`、`DELETE /{id}` | +| 前缫合计 / 后缫合计自动求和(`Page_Spjc_Edit` 的 getter) | 前端即时重算 + 服务端落库前再算一次 | +| **丝片台帐**:按账期月份列出每日台帐;新增时上一日 / 上存取上一本台账 | `GET /api/prod/sptz/list?dateFrom=&dateTo=`、`GET /defaults?date=`、`POST /save`、`DELETE /{id}` | +| 台帐派生值:上日遗留/今日新增 = 上存 − 浸丝;今存 = 下丝 + 上日遗留/今日新增 | 前端即时算 + 服务端保存时重算(同 F9 `CalSryl_Jrxz` / `CalJc`) | +| 台帐「整理片数」可按当日丝片检查汇总取数(F9 弹窗里的取数按钮) | `GET /defaults` 一并返回 `zlTichu` / `zlZhengpin` | +| 新增台帐时若已存在更晚日期的台帐 → 拦截 | 同上(`LatestDate`),前端也做二次校验 | + +**计算口径**: + +``` +秤大丝:车台.个数 = 丝量1~4 中 >0 的个数;车台.小计丝量 = Σ丝量 + 汇总.已秤数 = Σ个数;汇总.已秤丝量 = Σ小计丝量(g) + 应秤数 = 车台数 × 4 ×(半车 0.5 / 整车 1);待秤数 = 应秤数 − 已秤数 + 茧量 = 落丝桶数 × 每桶茧量(g) + 缫折 = 茧量 ÷ 丝量 × 100(≤100 视为异常,不落指标,与 F9 一致) + 台产 = 丝量 ÷ 落丝时长 ÷ 车台数 +丝片台帐:上日遗留/今日新增 = 上存 − 浸丝;今存 = 下丝 + 上日遗留/今日新增 + 整理剔除 = Σ当日丝片检查前缫合计 + 整理正品 = Σ(总片数 + 学工片数) − 剔除 +``` + +--- + +## 二、后端 + +### 2.1 实体(`Domain/Entities/Prod/`) + +| 类 | 文件 | 表 | 主要字段 | +|---|---|---|---| +| `CdsLsjl` | `Cds.cs` | `CdsLsjls` | LsjlId(唯一)、Num、Siliang、Jianliang、Saozhe、Taichan | +| `CdsChetai` | `Cds.cs` | `CdsChetais` | LsjlId + Chetaihao(唯一)、Sl1~Sl4、Num、Siliang | +| `Glxd` | `Glxd.cs` | `Glxds` | LsjlId(唯一)、Gongliangxiandu | +| `Spjc` | `Spjc.cs` | `Spjcs` | Date + DeptId + GyzkId + Guige(唯一)、总/学工片数、Qs×7 + QsHeji、Hs×12 + HsHeji、Memo | +| `Sptz` | `Sptz.cs` | `Sptzs` | Date(唯一)、DateLast、Js×5、Fs/Bs 片数、Zl 正品/剔除、Memo | +| `LsjlRecord` | `LsjlRecord.cs` | `LsjlRecords` | **新增**:Tongshu(落丝桶数)、Meitongjianliang(每桶茧量 g) | + +> 均继承 `BaseEntity`(自增主键 + 软删除),建表沿用 `DbSeeder.SyncStructure`; +> `LsjlRecords` 是存量表,`SyncStructure` 不给旧表加列,故在 `SyncStructure` 末尾追加两条 +> `EnsureColumn(fsql, "LsjlRecords", "Tongshu"/"Meitongjianliang", "INTEGER NOT NULL DEFAULT 0")`。 + +### 2.2 服务与接口(路由统一到 `api/prod/*`) + +``` +GET /api/prod/cds/list?pageIndex=&pageSize=&date=&deptId= 秤大丝列表(落丝记录 + 秤丝汇总) +GET /api/prod/cds/{lsjlId} 称重弹窗(首次按车台范围初始化空行) +POST /api/prod/cds/save 车台明细先删后建 + 汇总指标重算 +DELETE /api/prod/cds/{lsjlId} 清除称重数据(汇总 + 明细,F9 的 Cds_Lsjl_Delete) + +GET /api/prod/glxd/list?date=&deptId= 公量纤度列表(含纤度检验平均纤度) +POST /api/prod/glxd/save 按日期 + 车间整日批量 upsert + +GET /api/prod/spjc/list?pageIndex=&pageSize=&date=&deptId=&like= +GET /api/prod/spjc/{id} 修改回填 +POST /api/prod/spjc/save 合计服务端重算 +DELETE /api/prod/spjc/{id} 物理删除(业务键带唯一索引) + +GET /api/prod/sptz/list?dateFrom=&dateTo=&pageIndex=&pageSize= 账期区间查询 +GET /api/prod/sptz/{id} 修改回填 +GET /api/prod/sptz/defaults?date= 上一本台账 + 上存 + 丝片检查取数 +POST /api/prod/sptz/save 派生值服务端重算 +DELETE /api/prod/sptz/{id} 物理删除(日期唯一) +``` + +要点: +- 唯一性校验在前:丝片检查按「日期+车间+庄口+规格」、丝片台帐按「日期」查重(修改时排除自身),命中返回 400; +- 删除走 `HardDelete`(与煮茧日报同一策略),避免「删除后重建」撞唯一索引; +- 秤大丝「车台明细先删后建」,保证移除的行真正删除。 + +### 2.3 菜单 + +`DbSeeder.SeedProdMenus` 生产-后缫子菜单扩到 6 项,并做排序归一;`BusinessSeedData.SeedMenus` 同步追加同名菜单,避免两套种子不一致: + +| 顺序 | 菜单 | 路径 | 组件 | 权限标识 | +|---|---|---|---|---| +| 1 | 纤度检验 | /prod/houxian/xdjy | prod/xdjy/index | prod:xdjy:list | +| 2 | 黑板检验 | /prod/houxian/hbjy | prod/hbjy/index | prod:hbjy:list | +| 3 | 秤大丝 | /prod/houxian/cds | prod/cds/index | prod:cds:list | +| 4 | 公量纤度 | /prod/houxian/glxd | prod/glxd/index | prod:glxd:list | +| 5 | 丝片检查 | /prod/houxian/spjc | prod/spjc/index | prod:spjc:list | +| 6 | 丝片台帐 | /prod/houxian/sptz | prod/sptz/index | prod:sptz:list | + +--- + +## 三、前端 + +| 文件 | 说明 | +|---|---| +| `views/prod/cds/index.vue` | 秤大丝列表(日期 + 车间)+ 称重弹窗(车台 × 丝量1~4、个数/小计即时重算、汇总栏、偏轻/偏重着色)+ 清除称重 | +| `views/prod/glxd/index.vue` | 公量纤度:按「日期 + 车间」一次加载,表格内直接编辑,整日批量保存 | +| `views/prod/spjc/index.vue` | 丝片检查:多级表头(前缫 / 后缫)+ 新增/修改/删除弹窗(合计只读实时汇总)+ 工艺庄口选择页 | +| `views/prod/sptz/index.vue` | 丝片台帐:账期月份查询 + 多级表头(浸丝/返丝/编丝/整理)+ 新增/修改/删除,派生值只读,「取丝片检查」按钮 | +| `views/prod/lsjl/index.vue` | 落丝记录表单补「落丝桶数 / 每桶茧量」(秤大丝算茧量、缫折、台产的输入) | +| `router/index.js` | `componentMap` 注册 `prod/cds/index`、`prod/glxd/index`、`prod/spjc/index`、`prod/sptz/index` | + +交互要点: +- 丝量 ≤900 g 标绿(偏轻)、≥1200 g 标橙(偏重),与 F9 `CellFormatting` 一致; +- 丝片检查 / 丝片台帐的合计、上日遗留今日新增、今存均为只读派生值,避免与服务端口径冲突; +- 丝片台帐新增时不允许早于已有最新台账日期(对齐 F9「已生成 X 台账,无法新增该日期台账」)。 + +--- + +## 四、验证记录 + +| 项 | 结果 | +|---|---| +| `dotnet build`(Domain / Application / Infrastructure) | ✅ 0 错误 | +| `dotnet build`(Api,临时输出目录 `obj\verify\`) | ✅ 0 错误 | +| IDE 诊断(新增 4 个 vue + `router/index.js` + `lsjl/index.vue`) | ✅ 0 | +| `vite build` | ✅ 通过(仅既有 chunk 体积警告) | +| 启动种子 | ✅ `[DbSeeder] 生产模块菜单已就绪`,无结构同步异常;`EnsureColumn` 补列生效 | +| 菜单与权限 | ✅ `/auth/login` 返回的 permissions 含 `prod:cds:list` / `prod:glxd:list` / `prod:spjc:list` / `prod:sptz:list` | +| 四个列表接口 | ✅ 200(`cds` total=3、`glxd` / `spjc` / `sptz` 空表 200) | +| 丝片检查保存 | ✅ 前缫合计 13、后缫合计 55(服务端重算) | +| 丝片检查唯一性 | ✅ 同「日期+车间+庄口+规格」再存被拒(400),未产生第二条 | +| 丝片台帐派生值 | ✅ 上存 120 − 浸丝 40 = 80;下丝 80 + 80 = 今存 160 | +| 丝片台帐滚动 | ✅ 次日 `defaults`:`dateLast=2026-10-20`、`jsShangcun=160`(= 上一日今存) | +| 丝片台帐取数 | ✅ 当日丝片检查 Σ前缫合计 = 13 → 剔除 13;Σ(总片数+学工片数) 110 − 13 = 正品 97 | +| 秤大丝称重 | ✅ 车台 21(900/1100)+ 22(1000)→ 已秤数 3、已秤丝量 3000 g | +| 秤大丝指标 | ✅ 茧量 100×600 = 60000、缫折 = 60000/3000×100 = 2000、台产 = 3000/9/20 = 16.67 | +| 删除后无残留 | ✅ 删除秤大丝/丝片检查/丝片台帐后列表回到 0 条,「删除后重建」不再撞唯一索引 | +| 测试数据清理 | ✅ 验证用的丝片检查、丝片台帐、秤大丝数据已全部删除;`LsjlRecords.Tongshu/Meitongjianliang` 已还原为 0 | + +> 验证方式:另编一份 Api 到 `obj\verify\`,以真实库在 3000 端口启动(原无进程占用该端口),验证完已停止实例并删除临时输出与日志。 +> Node 不在默认 PATH 里,需先 `set PATH=C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6;%PATH%` 才能跑 vite;最终 `vite build` 已通过。 + +--- + +## 五、说明与后续 + +1. **落丝记录新增两个字段**:`Tongshu`(落丝桶数)、`Meitongjianliang`(每桶茧量)是秤大丝算茧量/缫折/台产的必需输入,F9 的 `Lsjl` 实体也有这两个字段(本系统此前漏移植),已在落丝记录表单补上录入项。 +2. **秤大丝删除**:F9 客户端没有删除按钮(只有 `Cds_Lsjl_DeleteCommand` 未暴露),本系统在列表加了「删除」,用于清除误录的称重数据;默认按 F9 的粒度整条(汇总 + 车台明细)物理删除。 +3. **未移植 F9 的本地设备能力**:`Page_Cds_Edit` 依赖串口电子秤 + 语音播报 + 本地缓存(`CacheLocalfileService`),Web 端不支持,改为直接录入;偏重/偏轻的语音提示折算UI 上的单元格着色。 +4. **未移植 HS\XCJC(现场检查)**:F9 只有 `Xcjc` 实体、没有对应页面(未见 `Page_Xcjc*`),本次不做;如有需要可单独加一组「温度/湿度/车速」维护页。 +5. **待接入**:① 秤大丝/丝片检查/丝片台帐的 Grid++ 打印`(报表中心目前只有 `lsjl.grf`);② 历史的 `biz_prod_hs_cds_*` / `biz_prod_hs_spjc` / `biz_prod_hs_sptz` 数据导出后可按工作进度-13 的 CSV 内嵌 + 幂等策略导入;③ 缫折/台产可作为成本核算与计件工资的数据源。 diff --git a/工作进度/工作进度-16.md b/工作进度/工作进度-16.md new file mode 100644 index 0000000..fb3db8d --- /dev/null +++ b/工作进度/工作进度-16.md @@ -0,0 +1,211 @@ +# 工作进度-16:薪资-挡车车头产量计算(CALCLDCCT)及其全部依赖 + +> 时间:2026-10-24(承接工作进度-15) +> 需求:把 `D:\AntaiProject - 副本\trunk\WinClient\F9.Business.PayRoll\PAYROLL\CALCLDCCT` 模块移植进本系统, +> 并补齐它依赖的全部功能(配置 + 数据源)。 +> 参考实现:`WinClient\F9.Business.PayRoll\PAYROLL\CALCLDCCT\Page_CalcLdcct.cs`(3067 行)、`Page_CalcLdcct_Edit.cs`、 +> `Application\CQRS\Business\PayRoll\CALCLDCCT\`(CalcLdcct_SaveCommand / CalcLdcct_DeleteByDateRangeDepCommand …)、 +> `Domain\Entities\Business\PayRoll\`(CalcLdcct / CONFGYZK / CONFGZ / CONFDCG / DCXDJK / XinZiJiaGePeiZhi)。 +> 落库表:F9 `biz_payroll_calcldcct` → 本系统 `CalcLdccts`。 + +--- + +## 一、功能设计 + +``` +薪资 +├── 挡车车头产量计算 /payroll/calcldcct 本次新增(核心) +├── 工艺庄口薪资配置 /payroll/confgyzk 本次新增(依赖配置) +├── 工种薪资配置 /payroll/confgz 本次新增(依赖配置) +├── 挡车岗位薪资配置 /payroll/confdcg 本次新增(依赖配置) +├── 挡车纤度奖扣 /payroll/dcxdjk 本次新增(依赖配置) +└── 薪资价格配置 /payroll/xzjg 本次新增(依赖配置) +``` + +| F9 行为 | 本系统实现 | +|---|---| +| 查询:年月(→账期)+ 部门 + 工种 + 姓名/庄口 Like | `GET /api/payroll/calcldcct/list` | +| 一键计算(挡车工):部门穿梭框多选 → 逐部门计算 → 对账报告 | `POST /api/payroll/calcldcct/calc-dcg`(单部门;返回条数/丝量/金额/告警) | +| 一键计算(车头工):**强依赖已存的挡车工产量** | `POST /api/payroll/calcldcct/calc-ctg` | +| 录入:单部门计算后弹窗人工微调(可改产量初始值 / 系数 / 金额-产量) | 表格内 3 列直接编辑 + 「保存微调」整账期覆盖(`POST /save`) | +| 删除:按 账期 × 部门 × 工种 整批删除 | `DELETE /api/payroll/calcldcct?start=&end=&depId=&worktypeId=` | +| 5 套薪资配置各自的增删改查 | 5 个 `api/payroll/conf*` `/dcxdjk` `/xzjg` 控制器 | + +### 计算口径(全部对齐 F9,服务端兜底重算) + +``` +【丝量】 +每桶丝量 = 每车丝量(kg) × 1000 / 标准落丝桶数 +甲乙合计丝量 = Round(Σ今缫桶数 × 每桶丝量, 0) (煮茧日报不分班) +班次丝量 = 甲乙合计 × Σ(挡车数×工时)_班 / Σ(挡车数×工时)_机组日 (挡车) + = 甲乙合计 × Σ(20×工时)_班 / Σ(20×工时)_机组日 (车头,固定 20 台/组) +个人丝量 = 班次丝量 × 挡车数×工时 / Σ(挡车数×工时)_同机组同班 (挡车) + = 班次丝量 × 工时 / (最长工时 × 2) (车头,2 人分摊) +看台时长 = Round(个人工时 × 庄口今缫桶数占比, 1),单庄口时等于个人工时 + +【等级】 +挡车工:木桶短板法 —— 均方差/总差/清洁分/洁净分/二度/三度各自在「生丝定级指标(标准=个人)」里定级,取最低分 +车头工:按挡车工丝量加权平均等级分后回落取整档;无挡车工产量时取庄口配置的 QsDengji/QsDengjifen +未下丝时:等级/等级分/缫折全部回落到庄口配置 + +【单价】 +挡车:绪数 = 挡车数 × 20;<60→50档基数,<70→60档,<100→70档,否则 100 档 + 基础单价 = 岗位基数金额 / 标准出勤天数 / 挡车数 / 标准上班时长 / 庄口工艺台产 +车头:基础单价 = 车头岗位金额 / 标准出勤天数 / 9 / 20 / 庄口工艺台产 × 2 +等级单价增减 = (等级分 − 工艺等级分) / 0.1 × 等级奖扣单价 / 10 / 1000 +实际单价 = 基础单价 + 等级单价增减 + +【质量】 +缫折 = Σ(车台茧量×看台比例) × 100 / Σ(车台丝量×看台比例) + 车台茧量 = 落丝茧量 × 该车台丝量占落丝丝量的比例(茧量只记在落丝层,需按丝量摊到车台) +中心纤度数 = 纤度值 == 生丝规格.中心纤度 的计数 × 看台比例 +野纤数 = 纤度 ≤ 野纤下限 或 ≥ 野纤上限 的计数 × 看台比例 +清洁扣款片数 = (特大糙 + 大糙 + 长结) × 看台比例 +洁净扣款片数 = (85 + 80 + 75 + 70 分档) × 看台比例 +二度/三度 = 黑板检验匀二度/匀三度 × 看台比例 +两班(甲乙)时:中心纤度/野纤/清洁/洁净/二度/三度及其扣款全部 × 0.5 + +【金额】 +产量金额初始值 = 实际单价 × 丝量; 产量金额 = 实际单价 × 丝量 × 计件系数 +缫折考核 = ±2kg 死区不奖不扣,超出扣掉死区后 clamp(挡车 ±10 / 车头 ±3),每 1kg 每 100kg 丝 5元(挡车)/1元(车头) +中心纤度奖 = 中心纤度数 × 1 元 +野纤扣 = −挡车纤度奖扣表扣款(≥2 条取扣款最大的 2 条求和) +清洁扣 = −(特大糙×2 + 大糙×0.5 + 长结×2) +洁净扣 = −(85×0.2 + 80×1 + 75×2 + 70×5) +二度扣/三度扣 = −二度×2 / −三度×20 +``` + +> 关键常量:`20`(每台绪数、车头 20 台/组)、`9`/`2`(车头)、`0.1`(等级分步长)、 +> `0.5`(两班折半)、`1.0/0.4/0.1`(清洁扣分基数)、`2`(缫折死区)、`5/1`(缫折单价)、`225`(F9 弹窗均值折算,本版未移植)。 +> 全程 `MidpointRounding.AwayFromZero`。 + +--- + +## 二、后端 + +### 2.1 实体(`Domain/Entities/PayRoll/`) + +| 类 | 文件 | 表 | 说明 | +|---|---|---|---| +| `CalcLdcct` | `CalcLdcct.cs` | `CalcLdccts` | 产量计算明细,唯一键 `Date+DepId+Banci+Jizu+EmployeeId+WorktypeId+GyzkId+Guige+Chetai` | +| `ConfGyzk` | `Conf.cs` | `ConfGyzks` | 工艺庄口薪资配置,唯一键 `Year+Month+GyzkId` | +| `ConfGz` | `Conf.cs` | `ConfGzs` | 工种薪资配置(40 项),唯一键 `WorkTypeId` | +| `ConfDcg` | `Conf.cs` | `ConfDcgs` | 挡车/车头岗位薪资配置,唯一键 `DepId` | +| `DangCheXianDuJiangKou` | `Conf.cs` | `DangCheXianDuJiangKous` | 挡车纤度奖扣,唯一键 `GuiGe+XianDu` | +| `XinZiJiaGePeiZhi` | `Conf.cs` | `XinZiJiaGePeiZhis` | 薪资价格配置,唯一键 `Group+Key` | + +均继承 `BaseEntity`,建表沿用 `DbSeeder.SyncStructure`。 + +### 2.2 服务 + +| 服务 | 文件 | 说明 | +|---|---|---| +| `ICalcLdcctService` / `CalcLdcctService` | `Services/PayRoll/CalcLdcctService.cs` | 列表 + 挡车/车头一键计算 + 保存 + 删除,内含全部计算口径 | +| `IConfGyzkService` / `ConfGyzkService` | `Services/PayRoll/PayRollConfService.cs` | 工艺庄口配置 CRUD | +| `IConfGzService` / `ConfGzService` | 同上 | 工种配置 CRUD(含 40 项金额) | +| `IConfDcgService` / `ConfDcgService` | 同上 | 挡车岗位配置 CRUD | +| `IDcxdjkService` / `DcxdjkService` | 同上 | 挡车纤度奖扣 CRUD | +| `IXzjgService` / `XzjgService` | 同上 | 薪资价格 CRUD + `GetPriceAsync(group,key)` | + +数据源(全部为系统已有模块,本次只做读取):看台记录 `KtjlDcg/KtjlCtg`、煮茧日报 `ZjrbRecord/ZjrbJizu`、 +落丝记录 `LsjlRecord`、秤大丝 `CdsLsjl/CdsChetai`、纤度检验 `XdjyLsjl/XdjyRecord`、黑板检验 `HbjyRecord`、 +生丝规格 `ShengsiGuige`、生丝定级指标 `ShengsiDingjizhibiao`、工艺庄口 `CraftZhuangKou`。 + +### 2.3 接口 + +``` +GET /api/payroll/calcldcct/list?start=&end=&depId=&worktypeId=&like=&pageIndex=&pageSize= +POST /api/payroll/calcldcct/calc-dcg 一键计算(挡车工) +POST /api/payroll/calcldcct/calc-ctg 一键计算(车头工) +POST /api/payroll/calcldcct/save 整账期×部门×工种全量覆盖(人工微调) +DELETE /api/payroll/calcldcct?start=&end=&depId=&worktypeId= + +GET /api/payroll/confgyzk/list?year=&month= POST /save GET /{id} DELETE /{id} +GET /api/payroll/confgz/list POST /save GET /{id} DELETE /{id} +GET /api/payroll/confdcg/list POST /save GET /{id} DELETE /{id} +GET /api/payroll/dcxdjk/list?guige= POST /save GET /{id} DELETE /{id} +GET /api/payroll/xzjg/list?group= POST /save GET /{id} DELETE /{id} +``` + +要点: +- 计算与保存都是「整账期 + 部门 + 工种全量覆盖」:先物理删除旧数据再重建,避免唯一索引冲突(与煮茧日报同一策略); +- 车头工计算必须已有挡车工产量,否则等级回落到庄口配置并给出告警; +- 所有告警通过 `CalcLdcctResultDto.Warnings` 返回,前端 `ElMessageBox.alert` 展示。 + +### 2.4 菜单 + +新增一级菜单「薪资」`/payroll`(排序 8,质量 7 与通知 9 之间),6 个子菜单: + +| 顺序 | 菜单 | 路径 | 组件 | 权限标识 | +|---|---|---|---|---| +| 1 | 挡车车头产量计算 | /payroll/calcldcct | payroll/calcldcct/index | payroll:calcldcct:list | +| 2 | 工艺庄口薪资配置 | /payroll/confgyzk | payroll/confgyzk/index | payroll:confgyzk:list | +| 3 | 工种薪资配置 | /payroll/confgz | payroll/confgz/index | payroll:confgz:list | +| 4 | 挡车岗位薪资配置 | /payroll/confdcg | payroll/confdcg/index | payroll:confdcg:list | +| 5 | 挡车纤度奖扣 | /payroll/dcxdjk | payroll/dcxdjk/index | payroll:dcxdjk:list | +| 6 | 薪资价格配置 | /payroll/xzjg | payroll/xzjg/index | payroll:xzjg:list | + +同时把一级菜单顺序归一为:`/qc` 7、`/payroll` 8、`/notification` 9、`/sys` 10、`/report` 11。 + +--- + +## 三、前端 + +| 文件 | 说明 | +|---|---| +| `views/payroll/calcldcct/index.vue` | 年月(→账期) + 部门 + 工种查询;多级表头(产量 / 质量 / 金额 / 金额-质量);一键计算(挡车/车头)、保存微调(产量初始值/系数/金额-产量三列直接编辑)、删除;底部显示当前页产量金额合计 | +| `views/payroll/confgyzk/index.vue` | 工艺庄口薪资配置 CRUD,庄口用 `CraftZhuangKouSelect` 选择 | +| `views/payroll/confgz/index.vue` | 工种薪资配置 CRUD,40 项按「出勤与工时/岗位与奖金/假期与加班/学工/岗位胜任考核/加班工资」分组 | +| `views/payroll/confdcg/index.vue` | 挡车岗位薪资配置 CRUD(50/60/70/100 绪基数金额 + 车头工金额) | +| `views/payroll/dcxdjk/index.vue` | 挡车纤度奖扣 CRUD | +| `views/payroll/xzjg/index.vue` | 薪资价格配置 CRUD,`薪资.前缫` 分组下预置「挡车工/车头工计件等级奖扣单价」 | +| `router/index.js` | `componentMap` 注册上述 6 个页面 | + +--- + +## 四、验证记录(全部由助手自行执行,无需人工点击) + +| 项 | 结果 | +|---|---| +| `dotnet build`(Domain / Application / Infrastructure / Api) | ✅ 0 错误 | +| `npm run build`(Node 在 `C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6`) | ✅ `✓ built in 14.88s`(仅既有 chunk 体积警告) | +| IDE 诊断(6 个 vue + router) | ✅ 0 | +| 菜单与权限 | ✅ `/auth/menus` 返回「薪资」+ 6 个子菜单 | +| 6 个接口连通性 | ✅ 全部 200(空表) | +| 挡车工一键计算 | ✅ 生成 1 条/庄口;丝量 = 桶数100 × (10kg×1000/4) = 250000 g | +| 挡车工单价 | ✅ 挡车数 20 → 绪数 400 → 100 档;基础单价 = 4000/28/20/9/100 = 0.0079365 | +| 挡车工等级 | ✅ 生丝定级指标 192 条参与木桶短板,定出 `6A00 / 12`;等级增减 = (12−5)/0.1×0.3/10/1000 = 0.0021 | +| 挡车工实际单价 / 产量金额 | ✅ 0.0100365;0.0100365 × 250000 × 1 = 2509.13 | +| 挡车工缫折 | ✅ 落丝茧量 60000 g、秤大丝丝量 25000 g → 缫折 240(**修复前为 4800,见下方问题**) | +| 挡车工缫折考核 | ✅ (300−240)−2 = 58 → 封顶 10 → 10 × (250000/1000/100) × 5 = +125 | +| 车头工一键计算 | ✅ 丝量 = 250000 × 9/(9×2) = 125000;基础单价 = 2000/28/9/20/100×2 = 0.0079365;缫折考核 = 3 × 1.25 × 1 = 3.75 | +| 车头工依赖挡车工 | ✅ 无挡车工产量时给出「工种薪资配置缺失」类告警并按配置回落 | +| 人工微调保存 | ✅ 产量初始值改 999.99 后保存,回读 `999.99 / 系数1 / 金额999.99` | +| 整账期删除 | ✅ 删除后 `total=0`,且「删除后重建」不再撞唯一索引 | +| 班次显示 | ✅ 修正为 `甲=1 / 乙=2`(初版误按 0/1 映射,已修) | +| 测试数据清理 | ✅ 播种的煮茧日报、看台记录、落丝记录、秤大丝、5 套配置、计算结果全部删除;验证脚本 `verify-payroll.ps1` 已删除 | + +### 过程中发现并修复的真实 bug + +1. **缫折被放大 20 倍**:初版按「车台数 × 落丝茧量」累加分子,导致 20 台时缫折 = 4800(正确 240)。 + 修正为:茧量只记在落丝层,按「车台丝量 / 落丝丝量」摊到车台后再按看台比例摊到人。 +2. **班次名称错位**:`Prod_Banci_Enum` 是 `甲=1 / 乙=2`,初版按 `0/1` 映射,甲班被显示成乙班。 + +--- + +## 五、移植取舍与后续 + +1. **未移植 F9 弹窗顶部的 4 个均值标签**(挡60/挡70/挡100/车头平均,`Σ金额 / Σ看台时长 × 225`):这是展示用的派生统计, + 本版在列表底部给出「当前页产量金额合计」;如需可按同样公式补在弹窗/工具栏。 +2. **未移植 F9 的「部门穿梭框多选」一键计算**:Web 端改为先选部门再点按钮,单部门计算(语义等价,逐个部门重复操作即可)。 +3. **野纤「奖励」分支未使用**:与 F9 一致——挡车工路径下 `JineZlYexian` 只取 `-扣款`,奖励分支被丢弃。 +4. **未移植的 F9 薪资子模块**(本次范围外,可按需继续):`CalXz`(薪资核算主表)、`Cmcl`(产量)、`QSXSCL`、`QTKH`、`SBRYKH` 等。 +5. **待接入**:① Grid++ 打印(报表中心目前只有 `lsjl.grf`);② 历史 `biz_payroll_calcldcct` 数据可按工作进度-13 的 CSV 内嵌 + 幂等策略导入; + ③ 本模块产出的「金额-产量 + 金额-质量」可直接作为计件工资与成本核算的数据源。 + +### 环境备注 + +- 本机 `node` / `npm` 不在默认 PATH,需先 `set PATH=C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6;%PATH%`。 +- Windows PowerShell 5.1 的 `Invoke-RestMethod` 默认按 ASCII 提交请求体,写中文会变成 `?`; + 验证脚本改为 `[System.Text.Encoding]::UTF8.GetBytes()` + `application/json; charset=utf-8`,并用 UTF-8 BOM 保存脚本。 +- 煮茧日报的「已存在相同的煮茧日报」校验会命中已软删记录,删除后无法在同一「日期+车间+庄口+规格」重建(既有模块行为,本次未改动)。 diff --git a/工作进度/工作进度-17.md b/工作进度/工作进度-17.md new file mode 100644 index 0000000..7f8c8f3 --- /dev/null +++ b/工作进度/工作进度-17.md @@ -0,0 +1,233 @@ +# 工作进度-17:仓储模块(WMS)五模块移植 + +## 一、任务目标 + +把 `D:\AntaiProject - 副本\trunk\WinClient\F9.Business.WMS` 下的五个子模块**按原业务逻辑一模一样**移植到本系统,UI 做适度美化: + +| 模块 | 原 F9 目录 | 本系统菜单 | +| --- | --- | --- | +| 原料 | `F9.Business.WMS\Yuanliao` | 仓储 / 原料 | +| 副产品 | `F9.Business.WMS\Fuchanpin` | 仓储 / 副产品 | +| 中间品 | `F9.Business.WMS\ZJP` | 仓储 / 中间品 | +| 生丝 | `F9.Business.WMS\SS` | 仓储 / 生丝 | +| 物资 | `F9.Business.WMS\WJ` | 仓储 / 物资 | + +## 二、建模方案(统一模型 + 模块/口径区分) + +五个模块的单据结构高度一致,因此没有建 5 套重复表,而是一套表 + 两个维度: + +- `Wms_Module_Enum`:原料=1 / 副产品=2 / 中间品=3 / 生丝=4 / 物资=5 +- `Kind`:0=库存台账,1=笼筒架台账(中间品专用) + +### 新增实体(Domain/Entities/Wms/Wms.cs) + +| 实体 | 表 | 说明 | +| --- | --- | --- | +| `WmsRuku` / `WmsRukuDetail` / `WmsBangmadan` | `WmsRukus` / `WmsRukuDetails` / `WmsBangmadans` | 入库单、入库明细、磅码单 | +| `WmsChuku` / `WmsChukuDetail` | `WmsChukus` / `WmsChukuDetails` | 出库单、出库明细 | +| `WmsKcTzDate` / `WmsKcTzItem` | `WmsKcTzDates` / `WmsKcTzItems` | 库存台账(日期 + 明细) | +| `WmsPici` / `WmsPiciBaojian` | `WmsPicis` / `WmsPiciBaojians` | 生丝批次、包件 | +| `WmsKucun` | `WmsKucuns` | 物资库存(按物资目录) | +| `WmsCaigou` | `WmsCaigous` | 原料采购 | +| `WmsFbplan` / `WmsFbjl` | `WmsFbplans` / `WmsFbjls` | 原料翻包计划 / 翻包记录 | +| `WmsCgplan` / `WmsCgplanItem` | `WmsCgplans` / `WmsCgplanItems` | 物资采购计划 / 明细 | + +### 台账项目口径(与 F9 一致) + +- 原料:按**原料庄口**;中间品笼筒架台账:按**工艺庄口**(且只统计「联统茧」物资) +- 其余:按**物资目录**;生丝额外按 `SubKey`(基地 / 外购)分组 + +### 两列数值口径 + +- 原料 / 副产品 / 中间品 / 生丝:**件数 / 净重**(生丝件数列取「批数」) +- 物资:**数量 / 金额** + +## 三、对齐 F9 的业务规则 + +### 1. 单据类型枚举(完整照搬 F9) + +| 模块 | 入库类型 | 出库类型 | +| --- | --- | --- | +| 原料 | 采购1 / 调拨2 / 其他3 | 领料1 / 销售2 / 样茧3 / 其他4 | +| 副产品 | 生产1 / 试缫2 / 其他5 | 销售1 / 调拨2 / 其他3 | +| 中间品 | 选茧0 / 试缫2 / 退料3 / 其他5 | 领料0 / 销售1 / 其他2 | +| 生丝 | 生产0 / 退货1 / 其他2 | 销售1 / 其他5 | +| 物资 | 采购1 / 冲账2 / 盘盈3 / 其他4 | 领料1 / 销售2 / 调拨3 / 报废4 / 其他5 | + +### 2. 重量 / 金额计算 + +- 磅码单:净重 = 毛重 − 皮重;明细件数 / 毛重 / 皮重 / 净重由磅码单汇总 +- 明细净重 = 毛重 − 皮重 − 包装重量 − 运损(未使用的项为 0) +- 物资:金额 = 数量 × 单价;库存单价 = 库存金额 ÷ 库存数量(4 位小数) +- 物资出库金额(`WJ_Chuku_Detail.CalJine`): + - 出库数量 = 库存数量 → 金额 = 库存金额,单价 = 金额 ÷ 数量 + - 否则 → 单价 = 库存单价,金额 = 数量 × 单价 +- 物资出入库实时更新库存(加权平均单价);删除 / 修改单据时回滚,保证严格对称 + +### 3. 库存台账生成(`YL_Kucun_Taizhang_Edit.GenerateTaizhang / CalTdayJiecun`) + +- 上日结存:取上一台账中「件数或净重不为 0」的行 +- 汇总区间:`LastDate + 1天` ~ `TzDate`(笼筒架台账为 `LastDate` ~ `TzDate`,与 F9 的 Yesterday~Date 一致) +- 无上一台账时 `LastDate = 1970-01-01`(与 F9 `Page_YL_Kucun_Taizhang.cs` 一致,首份台账汇总全部历史) +- 今日结存件数 = 上日 + 入库 − 出库 +- 今日结存重量 = Round(上日 + 入库 − 出库 + 盈亏, 2, AwayFromZero) +- 全部出库 = 上日结存 ≠ 0 且今日结存 = 0;勾选后把结存转入盈亏(取消则转回) +- 只能生成**晚于**最新台账日期的台账 + +### 4. 台账保存校验(与 F9 一致) + +- 结存重量恒等式:`上日 + 入库 − 出库 + 盈亏 = 今日结存` +- 结存重量不能小于 0 +- 原料:全部出库时样茧件数必须为 0 + +### 5. 生丝批次包件 + +- 净重 = 毛重 − 皮重;公量 = 净重 × (1 + 11%) ÷ (1 + 实测回潮率%) +- 批次库存状态:待入库1 / 已入库2 / 部分出库3 / 已出库4;生丝出库后按批次号更新为「已出库」 + +### 6. 物资扩展 + +- 采购计划(草稿 / 已确认 / 已完成)+ 计划明细 +- 采购报销:入库单 `BxStatus` 无0 / 待报销1 / 已报销2,支持批量报销 +- 库存查询、出入库流水(入库为正、出库为负) + +## 四、后端实现 + +| 文件 | 说明 | +| --- | --- | +| `Domain/Entities/Wms/Wms.cs` | 全部 WMS 实体与 `Wms_Module_Enum` | +| `Application/Dtos/Wms/WmsDtos.cs` | 出入库 / 明细 / 磅码单 / 台账 / 批次 / 采购 DTO | +| `Application/Services/Wms/WmsService.cs` | 统一服务:五模块出入库、台账、库存、流水、批次、采购 | +| `Api/Controllers/Wms/WmsController.cs` | `api/wms/*` 接口 | +| `Application/DependencyInjection.cs` | 注册 `IWmsService` | +| `Infrastructure/Data/DbSeeder.cs` | 一级菜单「仓储」+ 5 个子菜单;清理软删残留 | + +### 接口清单(`/api/wms`) + +``` +GET /options?module= 类型 / 物资 / 原料庄口 / 工艺庄口 / 部门 / 往来单位 +GET /ruku?module=&start=&end=&type=&like= +GET /ruku/{id} POST /ruku DELETE /ruku/{id} +POST /ruku/baoxiao 采购报销(物资) +GET /chuku?... GET /chuku/{id} POST /chuku DELETE /chuku/{id} +GET /taizhang/dates?module=&kind= +GET /taizhang?module=&kind=&date=®enerate= +POST /taizhang DELETE /taizhang/{id} +GET /kucun?module= GET /crk?module=&start=&end=&wzmlId= +GET /pici GET /pici/{id} POST /pici DELETE /pici/{id} +GET /caigou POST /caigou DELETE /caigou/{id} +GET /fbplan POST /fbplan DELETE /fbplan/{id} +GET /cgplan GET /cgplan/{id} POST /cgplan DELETE /cgplan/{id} +``` + +### 设计要点 + +- 单据编号(模块 + 日期 + 单号)唯一,重复时返回友好提示 +- 出入库单、批次采用**物理删除**:避免软删残留撞唯一索引而无法重建同号单据 +- 启动时清理历史软删残留(`WmsRukus` / `WmsChukus` / `WmsPicis`) + +## 五、前端实现 + +| 文件 | 说明 | +| --- | --- | +| `views/wms/moduleConfig.js` | 五模块差异化配置(项目口径、字段显示、数值口径、枚举选项) | +| `views/wms/components/RukuPanel.vue` | 通用入库(含磅码单弹窗、明细合计) | +| `views/wms/components/ChukuPanel.vue` | 通用出库(物资显示库存数、自动算金额) | +| `views/wms/components/TaizhangPanel.vue` | 通用台账(生成 / 编辑 / 盈亏 / 全部出库 / 保存 / 删除) | +| `views/wms/components/WjKucun.vue` | 物资库存 | +| `views/wms/components/WjCrk.vue` | 物资出入库流水 | +| `views/wms/components/WjCgplan.vue` | 物资采购计划 | +| `views/wms/components/WjCgbx.vue` | 物资采购报销 | +| `views/wms/components/SsPici.vue` | 生丝批次 + 包件(公量自动计算) | +| `views/wms/components/YlCaigou.vue` | 原料采购 | +| `views/wms/components/YlFbplan.vue` | 原料翻包计划 + 翻包记录 | +| `views/wms/{yuanliao,fuchanpin,zjp,ss,wj}/index.vue` | 五个模块页面(Tab 组织) | +| `router/index.js` | 注册 5 个页面组件 | + +### UI 优化点 + +- 统一卡片式布局 + Tab 分区,顶部带模块说明 +- 查询栏(日期区间 / 类型 / 关键字)与操作按钮分离,右侧对齐 +- 单据编辑弹窗:表头 4 列栅格表单,明细用可编辑表格(下拉 + 数字框),底部实时合计 +- 磅码单独立弹窗,自动算净重并汇总 +- 台账:结存列加粗高亮,盈亏 / 全部出库可交互,底部合计条 +- 状态用 `el-tag` 着色(库存状态、报销状态、翻包状态、采购计划状态) + +## 六、菜单 + +一级菜单「仓储」(`/wms`,排序 9,位于「薪资」之后),五个子菜单: + +| 名称 | 路径 | 组件 | +| --- | --- | --- | +| 原料 | `/wms/yuanliao` | `wms/yuanliao/index`(入库 / 出库 / 库存台账 / 原料采购 / 翻包计划) | +| 副产品 | `/wms/fuchanpin` | `wms/fuchanpin/index`(入库 / 出库 / 库存台账) | +| 中间品 | `/wms/zjp` | `wms/zjp/index`(入库 / 出库 / 库存台账 / 笼筒架台账) | +| 生丝 | `/wms/ss` | `wms/ss/index`(入库 / 出库 / 库存台账 / 批次包件) | +| 物资 | `/wms/wj` | `wms/wj/index`(入库 / 出库 / 库存台账 / 库存 / 出入库流水 / 采购计划 / 采购报销) | + +## 七、验证结果(全流程脚本自动执行,已清理测试数据) + +``` +[1] 登录 OK +[2] 菜单:仓储 children=5(原料 / 副产品 / 中间品 / 生丝 / 物资) +[3] 选项:原料模块 入库类型3 / 出库类型4 / 原料庄口347 / 物资1000 +[4] 原料入库:件数10 净重945(1000−50−5)✓ +[5] 原料出库:件数4 净重380 ✓ +[6] 台账生成:上日0 + 入库10/945 − 出库4/380 = 结存6/565 ✓ +[7] 台账保存 ✓ +[8] 次日台账:上日结存6/565 正确结转 ✓ +[9] 物资入库:库存 100 / 1000 / 单价10 ✓ +[10] 物资出库40:库存 60 / 600 / 单价10(CalJine 规则)✓ +[11] 生丝批次:净重120 公量119.46(59 + 60.46)✓ +[12] 原料采购 ✓ [13] 翻包计划 ✓ [14] 采购计划 ✓ [15] 批量报销 count=1 ✓ +[16] 清理完成,物资库存归零 ✓ +``` + +第二批(副产品 / 中间品 / 生丝 + 笼筒架台账): + +``` +[2 副产品] 入库6/570 出库2/190 结存4/380 ✓ +[3 中间品] 入库6/570 出库2/190 结存4/380 ✓ +[4 生丝] 入库2批/570 出库1批/190 结存1批/380(按基地分组)✓ +[3 笼筒架台账] 按工艺庄口统计 结存4/380 ✓ +``` + +- 后端 `dotnet build`:0 错误 +- 前端 `npm run build`:构建成功 +- 服务运行中:后端 `http://localhost:3000`、前端 `http://localhost:5173` + +## 八、后续修正(2026-10-26) + +1. **「在制品」更名为「中间品」** + - 枚举 `Wms_Module_Enum.在制品` → `中间品`(数据库存值仍为 3,不影响历史数据) + - 菜单「仓储 / 中间品」、页面标题、模块配置同步更名 + +2. **按仓储区分物资分组** + - 物资分组 `WuziGroup.BusinessType`:原料1 / 中间品2 / 生丝3 / 副产品4 / 五金5 + - `/api/wms/options?module=` 只返回本模块所属分组的物资: + + | 仓储模块 | 分组业务类型 | 物资数(现有数据) | 示例 | + | --- | --- | --- | --- | + | 原料 | 原料(1) | 2 | 0101 优茧、0102 普茧 | + | 副产品 | 副产品(4) | 39 | 040101 好五级茧 … | + | 中间品 | 中间品(2) | 1 | 0301 联统 | + | 生丝 | 生丝(3) | 6 | 070101 正品丝20/22 … | + | 物资 | 五金(5) | 952 | 02010063 轴承盒 … | + + - 笼筒架台账的物资过滤同步改为匹配名称含「联统」的物资(原为「联统茧」) + +3. **修复「新增页面点击确认保存报错」** + - 原因:下拉框清空后前端提交 `null`,而后端 DTO 的 `long/int/decimal` 不接受 `null`, + `System.Text.Json` 直接返回 400(`The JSON value could not be converted to System.Int64`) + - 修复:新增 `cleanPayload()`(`views/wms/moduleConfig.js`),提交前把 `null/undefined` 统一转成 `0` + (`scDate`、`guobangShijian`、`ckTime` 等日期型保持 `null`),所有 WMS 保存接口统一走该函数 + - 验证:中间品入库(件数3 净重285=300−15)、物资入库(10×5=50)均保存成功,测试数据已清理 + +## 九、说明与后续可选项 + +- 笼筒架台账只统计名称含「联统茧」的物资(与 F9 一致;未配置该物资时不过滤) +- 生丝公量按 11% 公定回潮率计算(F9 标准值) +- 已对齐 F9 的五个模块核心逻辑;如后续需要,可再补充: + - 生丝混装(`SS_Pici_Hunzhuang`) + - 物资调拨单、报废单的独立审批流 + - 各模块台账的报表打印模板 diff --git a/工作进度/工作进度-18.md b/工作进度/工作进度-18.md new file mode 100644 index 0000000..c74f73b --- /dev/null +++ b/工作进度/工作进度-18.md @@ -0,0 +1,170 @@ +# 工作进度-18:仓储 · 生丝 模块操作逻辑对齐 F9 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务目标 + +新系统(.NET 8 + Vue 3)的生丝模块功能已具备,但**页面操作逻辑与老系统不一致**。 +目标:以老系统 `D:\AntaiProject - 副本\trunk\WinClient\F9.Business.WMS\SS` 为基准,把生丝模块的**操作逻辑改成与老系统一模一样**。 + +对齐范围(本轮确认): + +- 生丝入库 / 出库的**批次驱动录入** +- 生丝**批次报检**的行内操作(过磅 / 包件 / 公量 / 质检 / 纤度检验) +- 出入库**主从联动** +- 财务审核:**本轮不做**,用户选择后续单独处理 + +## 二、核心差异(新系统改造前的问题) + +| 环节 | 老系统做法 | 新系统改造前 | +| --- | --- | --- | +| 入库明细 | 点「选择生丝批次」→ 勾选**待入库批次**,自动带出物资 / 批次号 / 批数 / 包件数 / 毛皮净重 | 手动「添加明细」→ 选物资 + 手填批次号 + 手填重量 | +| 出库明细 | 点「选择批次 + 包件」→ 勾选批次下未出库包件 | 手动录入,批次与包件脱节 | +| 批次状态 | 批次 / 包件级跟踪出库(`Ck_Isck`、`Ck_Baoshu` 等) | 仅按批次号字符串整体置"已出库",包件无跟踪 | +| 批号格式 | 正品 10 位数字 / 其他 `31EE-7位` | 无校验 | +| 主从联动 | 主表点行 → 下方明细联动 | 无,明细只在编辑弹窗 | +| 批次报检操作 | 过磅 / 包件 / 公量 / 质检 / 纤度检验 5 个行内操作 | 只有编辑、删除 | +| 规格 | 下拉选择(生丝规格) | 手填文本框 | +| 生产车间 | 取职责类型为「前缫」的车间 | 取全部部门,未筛选 | + +## 三、数据模型变更 + +### 新增字段(`Domain/Entities/Wms/Wms.cs`) + +| 实体 | 新增字段 | 对应 F9 | +| --- | --- | --- | +| `WmsPici` | `RkItemId`、`RkDate`、`CkLastDate`、`CkBaoshu`、`CkJingzhong`、`KcBaoshu` | `Rk_Item_Id`、`Rk_Date`、`CK_LastDate`、`Ck_Baoshu`、`Ck_Jingzhong`、`Kc_Baoshu` | +| `WmsPiciBaojian` | `CkIsck`、`CkItemId`、`CkDate`、`CkJingzhong` | 包件级出库标记 | +| `WmsRukuDetail` | `PiciIds` | `SS_Rk_Item.PiciIds` | +| `WmsChukuDetail` | `PcAndBjIds` | `SS_Ck_Item.PcAndBjIds` | + +### 新增实体 + +| 实体 | 表 | 说明 | +| --- | --- | --- | +| `WmsPiciChuku` | `WmsPiciChukus` | 批次出库记录(回滚时按此重算库存状态) | +| `WmsPiciQc` | `WmsPiciQcs` | 批次质检 | +| `WmsPiciQcXdjy` | `WmsPiciQcXdjys` | 批次纤度检验 | + +### 建表 / 加列(`DbSeeder.cs`) + +- 新表:由 `SyncStructure` 反射扫描 `BaseEntity` 子类自动建立 +- 旧表加列:`EnsureColumn` 兜底(幂等,列已存在则忽略) + +## 四、后端改动 + +### 1. 状态流转(`WmsService.cs`,对齐 F9 CQRS) + +- **入库**:`UpdatePiciRukuAsync` 按 `PiciIds` 置批次"已入库",写 `RkItemId` / `RkDate`,`KcBaoshu = Baoshu` +- **入库回滚**:`RollbackPiciRukuAsync`,若批次已出库则抛错禁止修改 / 删除 +- **出库**:`UpdatePiciChukuAsync` 按 `PcAndBjIds`(JSON)标记包件 `CkIsck`,写 `WmsPiciChuku`;批次按出库包件数流转「部分出库 / 已出库」 +- **出库回滚**:`RollbackPiciChukuAsync` 撤销包件标记、删除出库记录、按剩余记录重算批次状态 +- **校验**:待入库不可出库、已全部出库不可再出库 + +> 注意:`PcAndBjIds` 前端序列化为 camelCase(`piciId` / `piciBaojianIds`), +> 反序列化时必须带 `PropertyNameCaseInsensitive = true`,否则字段匹配不上。 + +### 2. 批号格式校验(`SavePiciAsync`) + +对齐 F9 `Page_PiciBaojian_Edit`: + +- 正品(`CodeType = 0`):`^\d{10}$` +- 其他(`CodeType = 1`):`^31EE-\d{7}$` + +### 3. 新增接口(`WmsController.cs`) + +| 方法 | 路由 | 说明 | +| --- | --- | --- | +| GET | `/api/wms/pici/chuku-select` | 出库选批次(含未出库包件),支持按物资过滤、排除已选批次 | +| GET | `/api/wms/pici/qc/{piciId}` | 读取批次质检 | +| POST | `/api/wms/pici/qc` | 保存批次质检 | +| GET | `/api/wms/pici/xdjy/{piciId}` | 读取纤度检验 | +| POST | `/api/wms/pici/xdjy` | 保存纤度检验 | + +### 4. 入库主表补"包数" + +`WmsRukuListDto` 新增 `BaojianNum`,由明细 `BaojianNum` 汇总(出库主表老系统无包数列,故未加)。 + +## 五、前端改动 + +### 1. 新建 `PiciSelectDialog.vue` + +生丝批次选择弹窗,两种模式复用: + +- `mode="ruku"`:拉待入库批次(`GET /wms/pici?kcStatus=1`),勾选批次 +- `mode="chuku"`:拉批次 + 包件(`GET /wms/pici/chuku-select`),展开行勾选包件, + 批次勾选 ↔ 包件勾选双向联动,已出库包件禁用 + +### 2. `RukuPanel.vue`(生丝入库) + +- 明细工具栏:生丝改为「选择生丝批次」,不再自动加空白行 +- 入库类型:仅显示「生产(0) / 退货(1)」(对齐 F9"其他暂未开放") +- 按物资分组生成明细,回填 `piciIds` / `piciCodes` / 批数 / 包件数 / 毛皮净重 +- 物资、批次号、包件数改为**只读**(由批次带出) +- 主表:新增「包数」列;新增展开行主从联动 +- 保存校验:生丝明细必须有 `piciIds` + +### 3. `ChukuPanel.vue`(生丝出库) + +- 明细工具栏:生丝改为「选择批次包件」 +- 出库类型:仅显示「销售(1)」 +- 往来单位(客户)改标签为「往来单位」并**必选** +- 明细回填 `pcAndBjIds`(JSON `[{piciId, piciBaojianIds}]`) +- 主表:展开行主从联动 + +### 4. `SsPici.vue`(批次报检) + +- 行内操作补齐:**过磅 / 包件 / 公量 / 质检 / 纤度检验** + - 过磅:仅「待入库」状态可用(否则提示并阻止) + - 包件、公量:复用批次编辑弹窗(编辑包件毛皮重 / 回潮率) + - 质检:新增质检弹窗(对接 `/wms/pici/qc`) + - 纤度检验:新增明细表格弹窗(对接 `/wms/pici/xdjy`) +- **规格**:由手填 `el-input` 改为 `el-select`,数据来自 `GET /shengsiguige/all` + (与 `prod/lsjl` 落丝记录取数口径一致) +- **生产车间**:由"全部部门"改为**仅取职责类型为「前缫」的车间** + ```js + import { depOptions, DUTY } from '../../../utils/departments'; + const opts = await depOptions({ duty: [DUTY.前缫] }, { flat: true }); + deptOptions.value = opts.map((o) => ({ id: Number(o.key), name: o.value, ...(o.raw || {}) })); + ``` + 列表列名同步改为「生产车间」 +- 保存校验补齐(对齐 F9 `AddValidateRuls`):日期、规格、品名(生丝规格)、批号、基地外购 + +> 变量名注意:组件内部门列表变量已改名 `deptOptions`,避免与工具函数 `depOptions` 重名冲突。 + +## 六、涉及文件 + +**后端** + +- `backend/F9WebAiTest.Domain/Entities/Wms/Wms.cs` +- `backend/F9WebAiTest.Application/Dtos/Wms/WmsDtos.cs` +- `backend/F9WebAiTest.Application/Services/Wms/WmsService.cs` +- `backend/F9WebAiTest.Api/Controllers/Wms/WmsController.cs` +- `backend/F9WebAiTest.Infrastructure/Data/DbSeeder.cs` + +**前端** + +- `web/src/views/wms/components/PiciSelectDialog.vue`(新建) +- `web/src/views/wms/components/RukuPanel.vue` +- `web/src/views/wms/components/ChukuPanel.vue` +- `web/src/views/wms/components/SsPici.vue` + +## 七、验证情况 + +- 后端 `F9WebAiTest.Application` 编译通过:**0 错误** +- 解决方案整体编译的报错均为 `MSB3021 / MSB3027`(**后端 API 进程正在运行,DLL 被锁定无法复制**),非代码问题 +- 前端 lint:0 错误 + +## 八、待办 / 遗留 + +1. **财务审核 / 反审核**:老系统生丝出入库均有「财务审核状态」列与批量审核 / 反审核按钮, + 新系统尚未实现(本轮用户选择暂缓,后续单独处理) +2. 生丝出库「选择调拨通知单」自动填充明细:老系统有(`Page_Dbtz_Select`),新系统未实现 +3. 老系统批次报检的「混装」功能在 F9 中已被注释,故未移植 + +## 九、下次接续提示 + +- 重启后端服务后,新表 / 新列才会在启动时建立 +- 继续做财务审核时,需新增通用审核记录与生丝出入库审核接口,并在两个列表页加「财务审核状态」列 + 批量审核 / 反审核按钮 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-19.md b/工作进度/工作进度-19.md new file mode 100644 index 0000000..258f430 --- /dev/null +++ b/工作进度/工作进度-19.md @@ -0,0 +1,153 @@ +# 工作进度-19:生产 · 选剥茧 模块移植(后端完成) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务目标 + +将老系统「生产 - 选剥茧」模块移植到新系统。老系统位置:`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\XBJ`。 + +模块构成(老系统两个子功能 + 三张表): + +| 老系统 | 说明 | +| --- | --- | +| `XBJ\XJRB\Page_Xjrb.cs` | **选茧日报**:某日某工艺庄口一条,记录干茧领入与 8 类茧 | +| `XBJ\XJRB\Page_XjrbPlan.cs` | **选茧计划**:按领料单(原料出库单)下达,含计划盈亏与完成标记 | +| `XBJ\XJRB\Page_XjrbItem_Edit.cs` | **计划明细**:挂在计划下的每日选茧进度行 | +| `XBJ\XJBZ\Page_xjpbbanzu.cs` | **选茧班组**:班次(甲/乙)+ 选茧组,含班组职工 | + +新系统此前**没有**该模块(`prod/zjrb` 是煮茧日报,不是选茧日报),需从零建。 + +## 二、核心业务规则(移植关键,务必照此实现) + +### 1. 选茧日报字段(8 类茧 + 退回) + +干茧领入(包数/重量)、联统茧、好五级茧、六级茧、绵茧、双宫茧、下脚茧、茧衣、茧花(各包数/重量)、 +退回(包数/重量)、合计重量、联统率、盈亏。 +合计重量 = 上述 8 类重量之和(**不含退回、不含领入**)。 + +### 2. 盈亏 / 联统率计算(对齐 F9 `GetXjrbCountYingkuiQuery` + `GetXjrbLingruStartDateQuery`) + +``` +步骤 1 起始日期(GetLingruStartDate): + history = 同庄口 且 Date < 当前日期 的日报,按日期降序(排除自身) + startDate = 当前日期 + for item in history(由新到旧): + if item.LiantongRate != 0: break // 遇第一条已结算记录停止 + startDate = item.Date // 否则继续往前推 +步骤 2 累计区间 = [startDate, 当前日期] 闭区间(排除自身) +步骤 3 累加(初值 = 当前表单输入值): + ltj = 当前联统茧重量 + Σ item.LtjZhongliang + yllr = 当前干茧领入重量 + Σ item.Yl_lr_zhongliang + all = 当前 8 类重量之和 + Σ item 同样 8 类重量之和 +步骤 4 公式: + 联统率 = yllr != 0 ? (ltj / yllr) * 100 : 0 + 盈亏 = all - yllr +``` + +**要点**:起始日期不是简单取"上月结存日",而是**往回跳过所有 `LiantongRate == 0`(未结算)的记录**, +遇到第一条已结算记录才停 —— 这是最容易实现错的地方。 + +### 3. 计划维度盈亏(口径与日报**不同**) + +``` +联统率 = 联统茧包数 ÷ (联统+好五级+五级+双宫+下脚+茧衣+茧花 包数之和) × 100 ← 按「包数」 +盈亏 = 累计选出总重量 − 计划原料领入重量(YlZhongliang) +``` + +> 老系统仓储里 `wjjBaoshu` / `wjjZhongliang` 的源字段**交叉写反**(疑似 bug), +> 本移植**已按正确字段聚合**(`SUM(WjjBaoshu)`→包数、`SUM(WjjZhongliang)`→重量), +> 若业务上必须与老系统完全一致(含 bug),可改回。 + +### 4. 标包重(老系统在客户端计算,尚未移植) + +老系统从注册表 `生产\选剥茧` 读取各品类标包重,**重量 = 包数 × 标包重**: + +| 注册表键 | 说明 | +| --- | --- | +| 联统茧标包重 | | +| 好五级茧标包重 | | +| 六级茧标包重 | | +| 棉茧标包重 | | +| 双宫茧标包重 | | +| 下脚茧标包重 | | +| 茧衣标包重 | | + +茧花、退回**无**标包重,手工录入。 +前端实现时需提供该配置入口(新系统暂无等价的注册表服务,待定:可做成常量配置或新增配置表)。 + +### 5. 选茧班组 + +- 班组 = 班次(甲/乙)+ 选茧组;班组职工来自「选剥茧车间」下工种为「选茧工」的在职职工 +- **没有独立新增入口**:由「编辑录入」勾选职工 + 填组别后整体保存(按班次覆盖:先清该班次旧职工与班组,再重建) +- 计划完成(`IsFinish`)后禁止修改/删除明细 + +## 三、本次完成(后端) + +### 新建文件 + +| 文件 | 内容 | +| --- | --- | +| `Domain/Entities/Prod/Xjrb.cs` | `XjrbRecord` / `XjrbPlan` / `XjrbItem` 三个实体(表 `XjrbRecords`、`XjrbPlans`、`XjrbItems`) | +| `Domain/Entities/Prod/XjpbBanzu.cs` | `XjpbBanzu` / `XjpbBanzuZhigong`(表 `XjpbBanzus`、`XjpbBanzuZhigongs`) | +| `Application/Dtos/Prod/XjrbDtos.cs` | 日报 / 计划 / 明细 / 班组 的列表与保存 DTO,含 `XjrbYingkuiDto` | +| `Application/Services/Prod/XjrbService.cs` | `IXjrbService` + 实现(盈亏、计划、班组全部逻辑) | +| `Api/Controllers/Prod/XjrbController.cs` | 路由前缀 `api/prod/xjrb` | + +### 修改文件 + +- `Application/DependencyInjection.cs`:新增 `services.AddScoped();` + (新系统 DI 是**手动注册**,不是反射扫描,新增 Service 必须补这一行) + +### 接口清单(`api/prod/xjrb`) + +| 方法 | 路由 | 说明 | +| --- | --- | --- | +| GET | `` | 日报分页(start / end / like 庄口关键字) | +| GET | `{id}` | 日报详情 | +| POST | `` | 保存日报 | +| DELETE | `{id}` | 删除日报 | +| POST | `count-yingkui` | 计算盈亏 / 联统率(保存前调用回填表单) | +| GET | `plan` | 计划分页 | +| GET | `plan/{id}` | 计划详情 | +| POST | `plan` | 保存计划 | +| DELETE | `plan/{id}` | 删除计划(已完成则拒绝) | +| GET | `plan/{planId}/items` | 计划明细列表 | +| POST | `plan/item` | 保存明细(勾选完成后回写计划盈亏) | +| DELETE | `plan/item/{id}` | 删除明细 | +| POST | `plan/count-yingkui` | 计划维度盈亏 | +| GET | `banzu` | 班组分页 | +| GET | `banzu/{id}/zhigong` | 班组职工 | +| POST | `banzu/zhigong` | 班组「编辑录入」批量保存 | + +## 四、踩坑记录 + +1. **`BaseEntity` 的命名空间**:在 `F9WebAiTest.Domain.Common`,实体文件必须 + `using F9WebAiTest.Domain.Common;`,光有 `FreeSql.DataAnnotations` 不够(会报 CS0246)。 +2. **DI 手动注册**:新增 Service 必须在 `DependencyInjection.cs` 里 `AddScoped`,否则运行时报未注册。 +3. **编译 Api 会被运行中的进程锁死 DLL**(MSB3021 / MSB3027)。 + 验证代码是否编译通过的正确做法:筛选 `error CS`(忽略 MSB),或单独编译 Application 项目。 + +## 五、验证情况 + +- `F9WebAiTest.Application` 编译:**已成功生成** +- `F9WebAiTest.Api` 编译:无 `error CS`(Controller 通过) +- 新表由 `DbSeeder.SyncStructure` 反射扫描 `BaseEntity` 子类**自动建表**,无需手写迁移 + +## 六、待办(下一轮) + +1. **前端页面**(`web/src/views/prod/xuanjian/`): + - 选茧日报列表 + 编辑弹窗(含「计算盈亏」按钮,调用 `count-yingkui` 回填) + - 选茧计划列表 + 计划明细(主从联动) + - 选茧班组(班组列表 + 职工联动 + 「编辑录入」弹窗) +2. **标包重配置**:确定新系统的存放方式(建议做成系统配置或常量),前端按 `重量 = 包数 × 标包重` 自动算 +3. **路由 + 菜单**:`web/src/router/index.js` 加路由,菜单种子数据加「生产 - 选剥茧」 +4. 「编辑录入」取职工:需要「选剥茧车间 + 选茧工工种」筛选员工的接口(需确认新系统员工/工种接口) +5. 重启后端服务后新表才会建立 + +## 七、下次接续提示 + +- 前端实现时,选茧日报的**「计算盈亏」按钮是核心交互**:填完包数/重量后点它,把返回的 + `liantongRate` / `yingkui` / `startDate`(提示"从 xxx 开始计算")回填到表单,再保存 +- 计划明细勾选「选茧完成」保存时会直接把计划置为已完成并回写盈亏,之后不可再改 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-20.md b/工作进度/工作进度-20.md new file mode 100644 index 0000000..f532fa7 --- /dev/null +++ b/工作进度/工作进度-20.md @@ -0,0 +1,76 @@ +# 工作进度-20:生产 · 选剥茧 前端完成(模块可运行) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +> 承接 [工作进度-19](./工作进度-19.md)(后端完成)。本轮完成前端,模块至此可运行。 + +## 一、本次完成(前端) + +新建目录 `web/src/views/prod/xuanjian/`: + +| 文件 | 内容 | +| --- | --- | +| `index.vue` | Tab 容器;三个菜单共用本组件,**按路由末段自动定位 Tab**(`/rb`、`/plan`、`/banzu`) | +| `XjrbPanel.vue` | **选茧日报**:查询、多表头列表(8 类茧各自包数/重量)、编辑弹窗、「计算盈亏」按钮 | +| `XjrbPlanPanel.vue` | **选茧计划**:列表 + **展开行主从联动**显示计划明细,明细增删改,明细内「计算盈亏」 | +| `XjpbBanzuPanel.vue` | **选茧班组**:班组列表 + 展开行显示职工,「编辑录入」弹窗按班次批量分配职工与组别 | + +### 关键实现 + +- **日报「计算盈亏」**:填完包数/重量后点按钮 → `POST /prod/xjrb/count-yingkui` → + 回填 `liantongRate` / `yingkui`,并 `ElMessage` 提示"从 xxx 开始累计"(即后端返回的 `startDateText`) +- **标包重**:老系统从注册表 `生产\选剥茧` 读取(重量 = 包数 × 标包重,茧花/退回手工录入)。 + 本次**先用 `localStorage`(key `xuanjian_biaobaozhong`)临时存放**并带「按标包重换算」按钮; + 折叠面板可编辑 7 个品类的标包重(联统/好五级/五级/绵/双宫/下脚/茧衣) +- **计划维度**联统率按**包数**口径(与日报按重量不同),明细勾选「选茧完成」保存后计划置为已完成、不可再改 +- **班组「编辑录入」**:按班次整体覆盖(后端先清该班次旧职工与班组,再重建) + +### 接口对接要点(踩坑) + +| 事项 | 正确写法 | +| --- | --- | +| 工艺庄口选择 | 用组件 `web/src/components/select/CraftZhuangKouSelect.vue`,`@confirm` 回调参数含 `id` / `zhuangKouName` / `productionDisplayName` | +| 员工列表 | `GET /sys/employee`,参数是 **`depId`**、**`worktypeId`**(注意不是 workTypeId)、`banci`、`workStatus`、`like` | +| 工种列表 | `GET /sys/worktype`,返回 `res.data.items` | +| 员工筛选口径 | 部门 = 职责类型「选剥茧」的车间(`depOptions({ duty: [DUTY.选剥茧] })`)+ 工种名含「选茧」 | +| 路由 | `web/src/router/index.js` 的路径映射表,新增 `'prod/xuanjian/index'` | + +## 二、注册与菜单 + +1. **路由**(`web/src/router/index.js`): + ```js + 'prod/xuanjian/index': () => import('../views/prod/xuanjian/index.vue'), + ``` +2. **菜单**(`backend/F9WebAiTest.Infrastructure/Data/BusinessSeedData.cs`): + 新建一级目录「生产-选剥茧」(`/prod/xuanjian`,Sort 7),下挂三个菜单: + - 选茧日报 → `/prod/xuanjian/rb` + - 选茧计划 → `/prod/xuanjian/plan` + - 选茧班组 → `/prod/xuanjian/banzu` + 组件均为 `prod/xuanjian/index` + +> ⚠️ `DbSeeder.cs` 里**另有一处**生产菜单的批量插入(`SeedProdMenus`,含"纤度检验/黑板检验…"元组列表)。 +> 若启动后菜单未出现,需在那一处同步补上选剥茧的三个菜单项(本轮只改了 `BusinessSeedData.cs`)。 + +## 三、验证情况 + +- 前端 4 个新文件 lint:**0 错误** +- `F9WebAiTest.Infrastructure` 编译:无 `error CS`(菜单种子数据通过) +- 后端(19 轮):Application 已成功生成、Api 无 CS 错误 + +## 四、待办 / 可优化 + +1. **标包重改为注册表**:新系统有 `/sys/registry` 接口(老系统正是存注册表 `生产\选剥茧`)。 + 当前用 localStorage 是临时方案,建议改为后端注册表读写,实现多端一致 +2. **领料单选择**:计划里的「领料单ID」目前是手填数字框,老系统是选择原料出库单, + 应改为下拉/弹窗选择并回填 `yllingliaodanCode` +3. **日报/计划关联**:老系统计划可按领料单生成日报,当前两侧独立,未做联动 +4. 菜单如未显示,补 `DbSeeder.SeedProdMenus` +5. 启动前需**重启后端服务**(新表在启动时建立;且 Api 进程会锁定 DLL 导致编译复制失败) + +## 五、下次接续提示 + +- 选剥茧三个页面都挂在 `prod/xuanjian/index.vue`,改 Tab 逻辑看 `tabOf(route.path)` +- 日报盈亏口径最易改错:起始日期由近及远跳过 `LiantongRate == 0` 的记录,见 19 轮文档第二节 +- 计划维度联统率用**包数**,日报用**重量**,两者分母不同,勿混用 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-21.md b/工作进度/工作进度-21.md new file mode 100644 index 0000000..fab188b --- /dev/null +++ b/工作进度/工作进度-21.md @@ -0,0 +1,74 @@ +# 工作进度-21:修复「生产-选剥茧」菜单未显示 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、问题 + +用户反馈:**项目里没有"生产-选剥茧"模块,前端看不到菜单页面**。 + +## 二、根因(重要,后续加菜单必须记住) + +项目中**有两处**菜单种子,但**只有 `DbSeeder` 是主流程**: + +| 文件 | 实体 | 是否主流程 | 是否处理角色权限 | +| --- | --- | --- | --- | +| `Infrastructure/Data/DbSeeder.cs`(`SeedProdMenus` → `EnsureTopMenuWithChildren`) | **`SysMenu`** | ✅ 是(生产菜单实际来源) | ✅ 自动 `EnsureMenuPermission` 关联 admin | +| `Infrastructure/Data/BusinessSeedData.cs`(`AddMenu`) | `Menu` | ❌ 否(上轮误改此处) | 需另走 adminRole 循环 | + +20 轮只改了 `BusinessSeedData.cs`,所以菜单**没有生效**。 + +**结论:以后新增菜单,必须改 `DbSeeder.cs` 的 `SeedProdMenus`,用 `EnsureTopMenuWithChildren`。** + +## 三、修复 + +在 `DbSeeder.cs`「生产-后缫」之后新增(Sort 7): + +```csharp +EnsureTopMenuWithChildren(fsql, snowflake, "/prod/xuanjian", "生产-选剥茧", "Grid", 7, + new (string Name, string Path, string Component, string Permission, string Icon, int Sort)[] + { + ("选茧日报", "/prod/xuanjian/rb", "prod/xuanjian/index", "prod:xuanjian:list", "Document", 1), + ("选茧计划", "/prod/xuanjian/plan", "prod/xuanjian/index", "prod:xuanjian:plan:list", "Calendar", 2), + ("选茧班组", "/prod/xuanjian/banzu", "prod/xuanjian/index", "prod:xuanjian:banzu:list", "User", 3) + }); +``` + +并补「子菜单顺序归一」(`/prod/xuanjian/rb`=1、`/plan`=2、`/banzu`=3)。 + +`EnsureTopMenuWithChildren` 会: +1. 建/更新一级目录(`/prod/xuanjian`,`MenuType_Enum.分组`) +2. 建子菜单(不存在才插入) +3. `EnsureMenuPermission` 关联 admin 角色 → 无需另外配权限 + +`BusinessSeedData.cs` 里 20 轮加的那段**保留**(`AddMenu` 有 `if (!Any(m => m.Path == path))` 幂等保护,不会重复)。 + +## 四、前端路由(已确认存在) + +`web/src/router/index.js`: +```js +'prod/xuanjian/index': () => import('../views/prod/xuanjian/index.vue'), +``` +菜单的 `Component` 字段 `prod/xuanjian/index` 与之对应,三个菜单共用该组件, +由 `index.vue` 内 `tabOf(route.path)` 按路由末段自动切 Tab(rb / plan / banzu)。 + +## 五、验证 + +- `F9WebAiTest.Infrastructure` 编译:无 `error CS` + +## 六、用户需要做的 + +1. **重启后端服务**(菜单在启动时 seed;且运行中 Api 进程锁 DLL 会导致编译复制失败) +2. 重启后**重新登录 / 刷新**(菜单随登录接口下发) +3. 用 **admin** 角色查看(权限只关联了 admin;其他角色需在角色管理里勾选) + +## 七、遗留 + +- 非 admin 角色需在「角色管理」手动勾选选剥茧三个菜单 +- 标包重仍是 localStorage 临时方案,建议改 `/sys/registry`(见 20 轮) +- 计划里「领料单ID」为手填数字框,老系统是选择原料出库单 + +## 八、下次接续提示 + +- 新增菜单一律改 **`DbSeeder.SeedProdMenus`**(`SysMenu` + `EnsureTopMenuWithChildren`),不要再只改 `BusinessSeedData` +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-22.md b/工作进度/工作进度-22.md new file mode 100644 index 0000000..863e138 --- /dev/null +++ b/工作进度/工作进度-22.md @@ -0,0 +1,75 @@ +# 工作进度-22:重启前后端(使选剥茧菜单与代码生效) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-21.md` 之外的 `工作进度-N.md`, +> 并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务 + +重启前端与后端,使 19~21 轮的选剥茧模块(新表 / 新接口 / 新菜单)生效。 + +## 二、进程定位(本项目实际端口) + +| 服务 | 端口 | 重启前 PID | 重启后 PID | +| --- | --- | --- | --- | +| 后端 `F9WebAiTest.Api` | **3000** | 23880(正是锁 DLL 的那个) | 8976 | +| 前端 vite | **5173** | 21392 | 13696 | + +- 后端端口来自 `backend/F9WebAiTest.Api/Properties/launchSettings.json` 的 `applicationUrl` +- 前端端口 vite 默认 5173 + +## 三、重启步骤(可复用) + +```powershell +# 1) 按 PID 停止(先 netstat 确认,勿误杀其它 dotnet/node) +Stop-Process -Id <后端PID> -Force +Stop-Process -Id <前端PID> -Force + +# 2) 启动后端(新窗口,非阻塞) +Start-Process -FilePath "dotnet" -ArgumentList "run" ` + -WorkingDirectory "d:/f9webaitest/f9-web-ai-test/backend/F9WebAiTest.Api" -WindowStyle Minimized + +# 3) 启动前端(见下方「坑」:npm/node 不在 PATH) +Start-Process -FilePath "C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe" ` + -ArgumentList "node_modules/vite/bin/vite.js" ` + -WorkingDirectory "d:/f9webaitest/f9-web-ai-test/web" -WindowStyle Minimized +``` + +## 四、坑(下次直接照做) + +1. **npm / node 不在 PATH**:本机 node 由 workbuddy 管理,路径为 + `C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe`。 + `Get-Command npm/node` 返回空 → 必须用**完整路径**启动,不能 `npm run dev`。 +2. **`netstat | Select-String ":5|:8"` 会漏掉 3000**:过滤端口时注意 3000 不含 5/8,别误判"后端没运行"。 +3. **后端 DLL 被运行中的 Api 进程锁定**(MSB3021 / MSB3027):编译前先停后端,或只编译 + Application/Infrastructure 项目并单独筛 `error CS`。 +4. 多个 dotnet 进程并存(3916 / 9636 / 11548 / 29256 …),**必须按端口定位**再杀,不能 + `Stop-Process -Name dotnet` 全杀。 + +## 五、验证结果 + +| 检查项 | 结果 | +| --- | --- | +| 后端 `GET /api/prod/xjrb` | **401**(已注册、需认证)→ Controller 已加载 | +| 前端 `http://localhost:5173` | **200** → vite 正常 | +| 数据库 `backend/F9WebAiTest.Api/f9_web_ai_test_rbac.db` | 最后修改 **2026/10/26 10:49**(重启时刻)→ seed 已执行,菜单已写入 | + +> 注:`bin/Debug/net8.0/` 下另有一份同名 db(10/20 的旧副本),**实际运行时用的是项目根目录下那份**。 + +## 六、用户待确认 + +1. **重新登录**(菜单随登录接口下发),用 **admin** 角色查看左侧是否出现「生产-选剥匣」→ + 选茧日报 / 选茧计划 / 选茧班组 +2. 非 admin 角色需在「角色管理」手动勾选这三个菜单 + +## 七、遗留(承接 20/21 轮) + +- 标包重仍是 localStorage 临时方案,建议改 `/sys/registry` +- 计划里「领料单ID」为手填数字框,老系统是选择原料出库单 +- 日报与计划暂未做联动 + +## 八、下次接续提示 + +- 改后端代码后必须**重启后端**才生效;改前端 vite 会自动热更新 +- 重启前记得先停后端,否则 DLL 锁定导致编译复制失败 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-23.md b/工作进度/工作进度-23.md new file mode 100644 index 0000000..a5a383b --- /dev/null +++ b/工作进度/工作进度-23.md @@ -0,0 +1,149 @@ +# 工作进度-23:移除现有考勤 + 移植「生产-职工考勤」调研(移植待续) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、任务 + +1. **移除**新系统现有考勤(`attendance`:打卡/请假/审批/记录/我的) +2. **移植**老系统「生产-职工考勤」:`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\KQ` + - `CQDJ` 考勤登记 + - `CQTJ` 考勤统计 + +## 二、本轮完成:移除现有考勤 ✅ + +### 后端删除 +- `Application/Services/Attendance/IAttendanceService.cs`、`AttendanceService.cs` +- `Application/Dtos/Attendance/AttendanceDtos.cs` +- `Api/Controllers/Attendance/AttendanceController.cs` +- `Domain/Entities/Attendance/`:`LeaveRequest.cs`、`LeaveApprover.cs`、`AttendanceRecord.cs` +- `DependencyInjection.cs`:删除 `AddScoped()`, + **并删除 `using F9WebAiTest.Application.Services.Attendance;`**(否则 CS0234) + +### 前端删除 +- `web/src/views/attendance/` 下 5 个页面:`approve/ leave/ my/ punch/ records/` 的 `index.vue` +- `router/index.js`:删除静态路由 `attendance/approve`、`STATIC_ROUTES` 置空、删除 5 条组件映射 +- `views/dashboard/index.vue`:删除「考勤打卡」快捷卡片 + +### 菜单 +- `DbSeeder.cs`:移除 `SeedAttendanceMenus(fsql);` 调用(方法体保留为死代码,未调用即不生效) +- ⚠️ **库中已存在的 `/attendance` 菜单记录不会自动删除**,需在「系统管理-菜单管理」手动删, + 或在 seed 中补清理逻辑(注意 `DbSeeder` 里菜单实体有 `SysMenu` 与 `Menu` 两种引用,别写错类型) + +### 编译 +- 解决方案编译:**无 error CS** ✅ + +> 注:`views/notification/type/index.vue` 里 `/attendance/approve?leaveId={businessId}` 只是 +> **placeholder 示例文本**,不影响运行,未改(建议后续改成中性示例)。 + +## 三、老系统「职工考勤」调研结论(移植依据) + +### 1. 实体 + +**考勤登记主表 `Cqdj`**(表 `biz_prod_kq_cqdj`,一人一天一条) +`Date / Dep_Id / Worktype_Id / Employee_Id / Banci / Is_xuegtong` +`Hours_standard / Hours_cq / Hours_qj / Hours_ld / Hours_jb / Hours_db / Hours_wp / Hours_kg / Hours_gs` +`Cs_yeban(夜班次数) / Memo` +唯一约束:`(Date, Employee_Id)`(一人一天一条);索引 `(Date,Dep_Id,Worktype_Id,Banci)` + +**登记明细 `Cqdj_Item`**(表 `biz_prod_kq_cqdj_item`,一人一天多行,按出勤类型拆) +`Cqdj_id / Dep_id / Worktype_id / Employee_id / Banci / Type_cq / Type_qj / Hours` +`Jizu / Ct_start / Ct_end / Dangche_num / Memo` + +**考勤统计 `Cqtj`**(表 `biz_prod_kq_cqtj`,**月快照**,非实时) +`Year / Month / Dep_id / Worktype_id / Employee_id` +`Days_cq_standard / Days_cq_actual / Hours_cq / Hours_qj` +`Hours_qj_shijia/bj/hj/cj/lx/sangjia/gj/nj/tx`(事病婚产轮休丧公年调) +`Hours_ld / Hours_jb / Hours_db / Hours_wp / Hours_kg / Cs_yeban` +`Hours_fadingjiaqi / Hours_qj_qqmc / Hours_qj_qqcal` +`Jine_quanqin / Jine_jb / Jine_jq_bz_hj/sangjia/bj/lx/fading/txnj / Jine_jq_bz` +`Jine_yb / Jine_kg_kk / Jine_liudong / Jine_daiban` +`Hours_xg_cq / Hours_xg_jiaban / Cq_tianci` + +### 2. 枚举(硬编码,无配置表) + +- 出勤类型 `Cqdj_CqType_Enum`:正常=1 请假=2 流动=3 加班=4 代班=5 外派=6 旷工=7 工伤=8 +- 请假类型 `Cqdj_QjType_Enum`:无=0 事假=1 病假=2 产假=3 婚假=4 丧假=5 公假=6 年假=7 调休=9 轮休=10 + - 校验:出勤类型=请假时请假类型≠无;非请假时请假类型必须为无 +- 班次 `Prod_Banci_Enum`:甲=1 乙=2 + +### 3. 考勤登记(CQDJ)规则 + +- 查询:日期 + **部门(必选)** + 工种(可空=全部) + 班次(默认甲) + 关键字 +- 主列表列:部门/工种/姓名/班次/学工/标准时长/出勤/请假/流动/加班/代班/外派/工伤/旷工/夜班/备注 +- 明细(选中职工联动):出勤部门/工种/姓名/班次/出勤类型/请假类型/时长/备注 +- **核心汇总公式**: + ``` + 正常明细.Hours = 标准 - 请假 - 旷工 - 流动 - 外派 - 代班 - 工伤 (倒挤) + Hours_cq = Σ(正常 + 流动 + 代班 + 外派 + 工伤) (不含加班) + 其余 Hours_qj/kg/ld/jb/wp/db/gs = 各类型明细求和 + 校验 Hours_cq >= 0 + ``` +- 约束:每人最多一条「正常」明细;不能删最后一条正常记录;保存前校验每人必须有正常记录 +- 新职工默认:`Hours_standard=9`、`Hours_cq=9`、明细 `Hours=9`、类型=正常/无;`Is_xuegong = !Base_is_zhuanzheng` +- **保存方式**:`Cqdj_SaveCommand` = 按 `(日期,部门,工种,班次)` **先软删旧登记与明细,再整批新增**(不做增量 diff) +- 按钮:设标准时长/设出勤时长/加班/流动/请假/公假/代班/夜班/添加职工/删除职工/刷新 +- 「添加其他车间」(`Page_Cqdj_addOtherChejian_select.cs`)**老系统中无任何页面引用**,属遗留未启用功能,可不移植 + +### 4. 考勤统计(CQTJ)规则 + +- 维度:**年/月(账期)+ 部门 + 工种 + 职工**(一人一行) +- 数据来源:**快照**。点「录入」时:若该 年/月/部门 已有统计则取库;否则由 `Cqtj_TongjiQuery` 取账期内 + 全部 Cqdj + Cqdj_Item + 薪资配置 + 职工,在客户端计算后 `Cqtj_Save`(先软删该年月部门旧数据再整批插入) +- 派生公式(关键): + ``` + 实际出勤天数 = Round(本部门 Hours_cq / 工种配置.HoursPerday, 2) + 出勤天次 = 该人该部门登记中 (Hours_cq + Hours_jb) > 0 的天数计数 + 夜班补助 = 工种配置.JineYeban × Cs_yeban + 旷工考核 = 工种配置.JiaqiJineKuangongKk × Hours_kg + 流动工资 = Σ 工种配置.LiudongJineGuding × 流动时长 + 代班工资 = Σ (代班部门工种.JineGangwei / (QqDaysStandard×HoursPerday)) × 代班时长 + 带薪时长 = 轮休+年假+公假+调休+婚假+产假+丧假 + 加班免扣请假 = 若(出勤+总加班) ≥ QsQuanqinDays×HoursPerday 且 总加班 ≥ QqHoursDkMinjiaban + → QqHoursDkQingjia,否则 0 + 全勤计算请假 = 总请假 - 加班免扣 - 带薪时长 + 假期补助 = 病假×BingjiaBz + 轮休×LunxiuBz + 婚假×HunjiaBz + 丧假×SangjiaBz + + (年假+调休)×NianjiaBz + 法定假补助 + (事假/产假/公假只计时长不计金额) + 加班费 = Σ 加班时长 × 单价(单价取岗位胜任考核 5 档对应 Jiaban_gwsrkhJine*,无考核取 JiabanJineBz) + 法定假期时长 = 职工配置.Hours_FadingBz + Hours_FadingGz + ``` +- **全勤奖 `quanqin()` 最复杂**(老系统在客户端 `Page_Cqtj.cs:837`,服务端 Command 是空实现): + 学工用 `XgJineQuanqin`;总旷工 ≥ 半天 → 0;总出勤 ≤ 3 天 → 0;当月离职且出勤 ≤15 天 → 0; + 单部门/工种:出勤 ≥15 天给满,否则 `全勤奖/(QqDaysStandard×HoursPerday) × 出勤时长`; + 多部门:`全勤奖/总出勤时长 × 本部门出勤时长`;入职日在账期内按标准折算;设备办全职特殊口径; + 最终:`全勤计算请假 ≥ QqHoursMaxQj → 0`,否则 `初始全勤奖 - 初始全勤奖/QqHoursMaxQj × 全勤计算请假` + +### 5. 依赖的基础数据(移植前必须确认新系统是否具备) + +| 依赖 | 说明 | +| --- | --- | +| 部门 | 排除「公司」类型,`Department_DutyType_Enum`(前缫=25/后缫=30/选剥茧=15/设备办=35) | +| 职工 | `Employee_GetByDepartmentIdView(DepId, WorkTypeId, Banci)`;用 `Base_is_zhuanzheng`(→学工)、`Base_RuzhiDate`、`LizhiDate`、`Base_WrokStatus`;`Base_WorktypeId=0` 过滤掉 | +| 工种 | `WorkType_GetByDepartmentIdView`;注册表 `基础数据\工种` 存 挡车工/车头工/值班长/选茧工 ID | +| 账期 `Biz_Base_Zhangqi` | `Zhangqi_GetByDate`、`Zhangqi_GetIsZhaZhangByDate`(**扎帐即禁止增删改**) | +| 薪资配置-工种 CONFGZ | HoursPerday、JineQuanqin、XgJineQuanqin、QqDaysStandard、QqHoursMaxQj、QqHoursDkMinjiaban、QqHoursDkQingjia、JiaqiJine*、JineYeban、JiaqiJineKuangongKk、LiudongJineGuding、JineGangwei、JiabanJineBz、Jiaban_gwsrkhJine*(5档) | +| 薪资配置-职工 CONFEMPLOYEE | `Hours_FadingBz`、`Hours_FadingGz`、`IsXuegong` | +| 薪资配置账期 CONFZQ | `QsQuanqinDays`(前缫出勤天数,必填) | +| 岗位胜任考核 | 决定加班单价档位 | + +## 四、待办(下一轮实施移植) + +1. 后端实体:`Cqdj` / `CqdjItem` / `Cqtj`(放 `Domain/Entities/Prod/`,继承 `BaseEntity`, + **必须 `using F9WebAiTest.Domain.Common;`**) +2. DTO + Service + Controller(路由建议 `api/prod/kaoqin`),含: + - 登记分页 / 按(日期,部门,工种,班次)整批保存(先软删后新增)/ 删除 + - 统计分页 / 生成(快照)/ 编辑保存 / 删除 +3. 前端页面(`web/src/views/prod/kaoqin/`):考勤登记(主从联动 + 明细按出勤类型)、考勤统计 +4. 菜单:在 `DbSeeder.SeedProdMenus` 用 `EnsureTopMenuWithChildren` 加(与选剥茧同法) + - ⚠️ 加菜单必须改 **`DbSeeder.SeedProdMenus`(`SysMenu`)**,不要只改 `BusinessSeedData` +5. 确认新系统是否已有薪资配置(CONFGZ/CONFZQ/CONFEMPLOYEE)——**统计页强依赖**,缺失则需先补齐或先做简化版统计 +6. 库中历史 `/attendance` 菜单手动删除 + +## 五、下次接续提示 + +- 出勤/请假/班次是**硬编码枚举**,前端下拉直接写死,不用查表 +- 登记保存是「整批覆盖」不是增量 diff,实现时别做反 +- 统计是**快照**(生成后落库),不是实时 SQL 汇总 +- 移植顺序建议:**先做考勤登记(CQDJ)**,统计(CQTJ)因强依赖薪资配置,放到薪资配置确认后再做 +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-24.md b/工作进度/工作进度-24.md new file mode 100644 index 0000000..af14fc9 --- /dev/null +++ b/工作进度/工作进度-24.md @@ -0,0 +1,78 @@ +# 工作进度-24:重启前后端 + 修复「报表设计管理器」打不开 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、重启前后端 ✅ + +| 服务 | 端口 | 重启后 PID | +| --- | --- | --- | +| 后端 `F9WebAiTest.Api` | 3000 | 26028 | +| 前端 vite | 5173 | 25640 | + +(复用 22 轮方法:按端口定位 PID → `Stop-Process` → `dotnet run` + node 完整路径启动 vite。 +npm/node 不在 PATH,必须用 `C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe`。) + +## 二、报表设计管理器:根因与修复 ✅ + +### 调用链 + +``` +报表管理中心 views/report/manage/index.vue 点列表行「设计」 + → openDesign(row):designOnOpen=true、selectedId=row.id、designerKey++ + → components/report/ReportEditor.vue 的 init() 末尾: + if (props.openDesign && savedId.value) designVisible.value = true; + → 全屏 el-dialog 渲染 components/report/ReportDesign.vue(v-if="designVisible && savedId") + → ReportDesign 内「用本机锐浪 7 设计器编辑」按钮 → openGridDesigner() + window.location.href = 'f9designer://open?base=..&rid=..&token=..&code=..' +``` + +### 根因(实测逐层排查得出) + +| 环节 | 实测结果 | +| --- | --- | +| `f9designer://` 协议注册 | ✅ **已注册**(`HKCR\f9designer`,含 `URL Protocol`) | +| 协议 command | `powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File "D:\f9webaitest\f9-web-ai-test\tools\report-designer\F9ReportDesignerHandler.ps1" "%1"` | +| **该宿主脚本** | ❌ **不存在**(`tools\report-designer\` 整个目录都没有)→ **根因** | +| 锐浪设计器 `gr7designer64.exe` | ✅ 已安装:`D:\GridReport7\gr7designer64.exe` | +| 后端接口 | ✅ 齐全(见下) | + +因为用了 `-WindowStyle Hidden`,powershell 启动失败时**没有任何提示** → 用户看到的就是"点了没反应/打不开"。 + +> 修正:此前 24 轮初稿判断"项目内没有配套协议注册文件"不准确 —— 协议已注册, +> 缺的只是被协议指向的那个**宿主脚本**(属于应入库但漏掉的资产)。 + +### 后端接口(脚本依赖) + +| 用途 | 接口 | 说明 | +| --- | --- | --- | +| 下载模板 | `GET /api/sys/report/{id}/template?access_token={token}` | `[AllowAnonymous]`,token 走 query;另有 `.grf` / `.json` 别名 | +| 上传模板 | `POST /api/sys/report/{id}/template` | `IFormFile` 参数名 **`file`**;注释明确"保留给 f9designer:// 协议回传使用" | + +### 修复:新建宿主脚本 + +`tools/report-designer/F9ReportDesignerHandler.ps1`(4434 字节,PowerShell 语法检查通过): + +1. 解析 `f9designer://open?base=&rid=&token=&code=`(URL 解码) +2. 定位设计器:默认 `D:\GridReport7\gr7designer64.exe`,可用环境变量 `F9_GR7_DESIGNER` 覆盖 +3. 下载模板到 `%TEMP%`(下载失败则建空白模板,兼容"新报表无模板") +4. `Start-Process -Wait` 打开设计器并**阻塞等待关闭** +5. 关闭后用 `System.Net.Http.MultipartFormDataContent` 上传(带 `Authorization: Bearer`) +6. 全程写日志 `tools/report-designer/logs/designer_yyyyMMdd.log` + +> 兼容性说明:用 .NET `HttpClient` 手工构造 multipart,而不是 PowerShell 7 的 `-Form`, +> 因为注册表里调用的是 `powershell.exe`(Windows PowerShell 5.1 不支持 `-Form`)。 + +### 待用户验证 + +在报表中心点「设计」→「用本机锐浪 7 设计器编辑」,应弹出锐浪设计器;关闭后模板自动回传。 +若仍无反应,看 `tools/report-designer/logs/designer_yyyyMMdd.log`。 + +## 三、下次接续提示 + +- 报表相关文件:`views/report/manage/index.vue`、`views/report/center/index.vue`、 + `components/report/ReportEditor.vue`、`components/report/ReportDesign.vue`、`utils/gridreport/*`、 + 后端 `Services/Reports/ReportService.cs` + `Controllers/SysReportController.cs` +- 设计面板打开前提:报表**必须已保存**(`v-if="designVisible && savedId"`),未保存的报表点了无反应 +- 设计面板有两条路径:① 可视化画布(纯前端、无外部依赖)② 本机锐浪设计器(需本协议链) +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-25.md b/工作进度/工作进度-25.md new file mode 100644 index 0000000..a5c357c --- /dev/null +++ b/工作进度/工作进度-25.md @@ -0,0 +1,127 @@ +# 工作进度-25:修复「报表设计打不开」+ 补上「生产-考勤 · 考勤登记」 + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、用户反馈 + +1. 报表设计页面打不开(之前可以用,今天突然不行) +2. 生产-考勤也没有 + +## 二、报表问题:两个根因,都已修复 ✅ + +### 根因 A:vite 开发服务器挂死(最直接的"打不开") + +| 检查 | 结果 | +| --- | --- | +| 后端 3000 | ✅ 正常(`/sys/report` 列表、模板下载、预览全部 200) | +| 前端 5173 早期请求 | ✅ 200(`report/manage`、`ReportEditor`、`ReportDesign`、`grlib/grhtml5-7-min.js`) | +| 稍后 `/`、`/src/views/prod/lsjl/index.vue` | ❌ **全部超时**(进程 25640 仍在 LISTENING,CPU 近乎 0 → 卡死不服务) | + +→ 页面请求全部挂住,表现为"点了没反应 / 打不开"。 + +**修复**:按端口定位 PID → `Stop-Process -Id 25640 -Force` → 用完整路径 node 重启 vite(npm 不在 PATH): + +```powershell +Start-Process -FilePath 'C:\Users\32453\.workbuddy\binaries\node\versions\22.22.2-6\node.exe' ` + -ArgumentList 'node_modules/vite/bin/vite.js' -WorkingDirectory 'd:/f9webaitest/f9-web-ai-test/web' -WindowStyle Minimized +``` + +重启后 PID **26328**:`/` 200、`prod/kaoqin/index.vue` 200、`report/manage/index.vue` 200。 + +### 根因 B:f9designer:// 宿主脚本解析 URL 失败(点「设计器编辑」无反应) + +日志 `tools/report-designer/logs/designer_20261026.log` 里今天 6 次唤起全部报错: + +``` +错误: 缺少 base / rid 参数(Url=f9designer://open/?base=...) +``` + +原因:**Windows Shell 会在协议主机后补一个 `/`**,实际收到的是 `f9designer://open/?base=...`, +而脚本按固定前缀 `^f9designer://open\?` 剥离失败 → `base` 解析为空 → 直接 throw。 +注册表里是 `-WindowStyle Hidden`,失败无提示 → 用户看到"点了没反应"。 + +**修复** `tools/report-designer/F9ReportDesignerHandler.ps1`: + +1. 解析改为「取第一个 `?` 之后的全部内容」,兼容 `open?` 与 `open/?` +2. `base` 缺失时回退 `http://localhost:5173`(并记日志) +3. 新增 `Show-Error`:失败时弹 Windows MessageBox(含日志路径),不再静默 +4. ⚠️ **踩坑**:编辑工具把文件存成了 **UTF-8 无 BOM**,Windows PowerShell 5.1 按 ANSI 读取导致中文注释/字符串乱码、脚本解析失败。 + 已用 `UTF8Encoding($true)` 重写为 **UTF-8 with BOM**,`ParseFile` 复检通过。 + **下次改这个脚本后务必确认 BOM 仍在**(`[System.IO.File]::ReadAllBytes($p)[0..2]` 应为 239,187,191)。 + +**端到端实测**(用 `F9_GR7_DESIGNER` 指向一个立刻退出的程序代替设计器): + +``` +base=http://localhost:5173 rid=141374578868883456 code=PROD_QS_1 +已下载 8300 字节 → 启动设计器(等待关闭)→ 设计器已关闭 → 上传结果: OK → 完成 +``` + +模板回传后长度仍为 7639 字节、报表 `hasTemplate=True`,未损坏。 + +## 三、生产-考勤:本轮补上「考勤登记(CQDJ)」✅ + +> 「没有」的原因:23 轮按计划**删除了旧考勤**(`/attendance`),而「生产-职工考勤」移植尚未开始。 + +### 后端(新增) + +| 文件 | 说明 | +| --- | --- | +| `Domain/Entities/Prod/Cqdj.cs` | `Cqdj`(表 `biz_prod_kq_cqdj`)+ `CqdjItem`(表 `biz_prod_kq_cqdj_item`),继承 `BaseEntity`,**自动建表**(`DbSeeder.SyncStructure` 反射扫描) | +| `Application/Dtos/Prod/KaoqinDtos.cs` | `CqdjItemDto / CqdjRowDto / CqdjViewDto / CqdjSaveDto / CqdjEmpOptionDto` | +| `Application/Services/Prod/KaoqinService.cs` | 接口+实现同文件:整组查询 / 整组覆盖保存 / 整组删除 / 可选职工 | +| `Api/Controllers/Prod/KaoqinController.cs` | `[Route("api/prod/kaoqin")]` `[Authorize]`:`GET|POST|DELETE /cqdj`、`GET /cqdj/employees` | +| `Application/DependencyInjection.cs` | `services.AddScoped();` | + +规则按 23 轮调研落地: + +- 查询口径:日期 + 部门(必选)+ 工种(0=全部)+ 班次(甲1/乙2)+ 关键字 +- 职工来源:本部门、工种≠0、非离职;未登记者生成默认值 9 小时 + 一条「正常」明细 +- **正常明细倒挤**:`正常 = 标准 - 请假 - 旷工 - 流动 - 外派 - 代班 - 工伤`;`出勤 = 正常+流动+代班+外派+工伤` +- 保存 = **先软删该(日期+部门+工种+班次)旧登记与明细,再整批新增**(非增量 diff) +- 校验:每人必须有且仅有一条「正常」;请假必须选请假类型、非请假必须为「无」;出勤不能为负 + +### 前端(新增) + +- `web/src/views/prod/kaoqin/index.vue`:上表(职工行 + 标准时长/夜班/备注 + 各时长实时汇总)下表(选中职工的明细:出勤类型/请假类型/时长/机组/车台/挡车数/备注), + 工具栏含 查询 / 设标准时长 / 添加职工 / 删除整组 / 保存 +- `web/src/router/index.js`:`'prod/kaoqin/index': () => import('../views/prod/kaoqin/index.vue')` + +### 菜单(`DbSeeder.SeedProdMenus`) + +```csharp +EnsureTopMenuWithChildren(fsql, snowflake, "/prod/kaoqin", "生产-考勤", "Clock", 8, + new[] { ("考勤登记", "/prod/kaoqin/cqdj", "prod/kaoqin/index", "prod:kaoqin:cqdj:list", "EditPen", 1) }); +``` + +同时**清理了遗留旧考勤菜单**(页面组件已删,留着会点开空白页): +删除 `sys_rbac_menus` 中 `path LIKE '/attendance%'` 的记录及其 `role_permissions`, +并删除 `code LIKE 'attendance:%'` 的权限单元。 + +### 验证 + +| 项 | 结果 | +| --- | --- | +| 解决方案编译 | **0 个错误** | +| `GET /auth/menus` | ✅ 出现 `生产-考勤 → 考勤登记`,旧「考勤管理」已消失 | +| `GET /prod/kaoqin/cqdj?date=2026-10-26&depId=34&banci=1` | ✅ 46 人,每人默认 9 小时 + 1 条正常明细 | +| `POST /prod/kaoqin/cqdj` | ✅ `count=46`;复查 `saved=46` | +| `DELETE /prod/kaoqin/cqdj` | ✅ `count=46`;复查 `saved=0`(测试数据已清理) | +| 前端模块编译 | ✅ vite 200 | + +## 四、待办 + +1. **考勤统计(CQTJ)**:月快照,强依赖工种薪资配置 `ConfGz`(已有)+ 职工薪资配置 CONFEMPLOYEE / 账期薪资配置 CONFZQ(**后端还没有,需先建**) +2. 老系统「添加其他车间」功能无页面引用,不移植 +3. 报表设计:可视化画布与锐浪设计器二选一(用设计器保存后产物为 `_WR7_`,画布将不可解析) +4. `views/notification/type/index.vue` 里的 `/attendance/approve?leaveId=` 仍是示例文本,建议改成中性示例 + +## 五、下次接续提示 + +- 改后端代码后**必须重启后端**(先按端口 Stop-Process,否则 DLL/obj 锁定导致编译失败);改前端 vite 热更新,但 **vite 卡死时要按端口杀掉重启** +- 登录 admin / 123456;后端 3000(PID 23968),前端 5173(PID 26328) +- 考勤相关文件:`Domain/Entities/Prod/Cqdj.cs`、`Dtos/Prod/KaoqinDtos.cs`、 + `Services/Prod/KaoqinService.cs`、`Controllers/Prod/KaoqinController.cs`、`views/prod/kaoqin/index.vue` +- 报表相关:`views/report/manage/index.vue`、`components/report/ReportEditor.vue`、`ReportDesign.vue`、 + `tools/report-designer/F9ReportDesignerHandler.ps1`(**务必保持 UTF-8 BOM**) +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-26.md b/工作进度/工作进度-26.md new file mode 100644 index 0000000..96fc50d --- /dev/null +++ b/工作进度/工作进度-26.md @@ -0,0 +1,106 @@ +# 工作进度-26:参考老系统完成整个考勤模块(CQDJ 补齐 + CQTJ 新建 + 依赖配置) + +> **协作规则(长期生效)**:**每次对话结尾都要生成工作进度**。 +> 即在每轮对话结束前,把本轮完成的内容追加为 `工作进度/工作进度-N.md`,并在文档中保留本条规则,方便下一轮接续。 + +## 一、需求 + +用户要求:**完善整个考勤模块,不要删减**,并给出老系统参考目录 +`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\KQ`(CQDJ 考勤登记 + CQTJ 考勤统计)。 + +## 二、老系统源码研读结论(KQ 目录) + +| 文件 | 大小 | 结论 | +| --- | --- | --- | +| `CQDJ\Page_Cqdj.cs` | 19KB | 查询口径:日期 + 部门 + 工种 + 班次;录入/删除整组、初始化为正常出勤;「从指定日期复制」 | +| `CQDJ\Page_Cqdj_Edit.cs` | 77KB | 工具栏 8 个按钮(标准/出勤/加班/流动/事假/公假/代班/夜班)+ 添加/删除职工 + 刷新;保存整组覆盖、倒挤「正常」 | +| `CQDJ\Page_Cqdj_addOtherChejian_select.cs` | — | 确认**无引用**,遗留功能,不移植 | +| `CQTJ\Page_Cqtj.cs` | 84KB | 统计是**月快照**;录入时库中有快照取库,否则 `CqtjViews` 实时汇总;含 `quanqin` 全勤奖、`jiaqiBuzhuJisuan` 假期补助、`jisuanJiabanMkHours` 加班免扣、`jisuanJianbanJine` 加班费、`liudongJineJisuan` 流动、`daibanJineJisuan` 代班、`jisuanCqTianci` 出勤天次、`Yijianshengcheng` 一键生成 | +| `CQTJ\Page_Cqtj_Edit.cs` | 7.5KB | 可编辑列:年假/公假/调休/法定假/全勤免扣/全勤计算/全勤奖/加班费/流动工资/代班工资/假期补助分项与合计/夜班补助/旷工考核 | + +CQTJ 强依赖(老系统在 PayRoll 侧): +- **CONFZQ 薪资配置-账期**:`QsQuanqinDays` 前缫出勤天数(为 0 时直接抛错不让录入) +- **CONFEMPLOYEE 薪资配置-职工**:`Hours_FadingBz` / `Hours_FadingGz` / `IsXuegong` +- **岗位胜任考核**:考核结果决定加班费单价档位(无记录时回落 `JiabanJineBz`) + +## 三、本轮新增 / 修改 + +### 后端(新增) + +| 文件 | 说明 | +| --- | --- | +| `Domain/Entities/Prod/Cqtj.cs` | **考勤统计实体**(`biz_prod_kq_cqtj`),含天数/时长/请假分项/学工/法定假/全勤相关时长/全部金额字段 | +| `Domain/Entities/PayRoll/ConfEmployee.cs` | `ConfZq`(`ConfZqs`)、`ConfEmployee`(`ConfEmployees`)、`Gwsrkh`(`Gwsrkhs`) | +| `Dtos/Prod/KaoqinDtos.cs` | 追加 `CqtjRowDto / CqtjSaveDto / CqtjGenerateDto / CqtjViewDto` | +| `Dtos/PayRoll/ConfEmployeeDtos.cs` | `ConfZqDto / ConfEmployeeDto / GwsrkhDto` | +| `Services/Prod/KaoqinService.cs` | 追加 CQTJ:分页、录入(取库 or 实时汇总)、保存(整组覆盖)、删除、一键生成、全部删除;并新增 8 个口径函数 | +| `Services/PayRoll/ConfEmployeeService.cs` | `IConfZqService / IConfEmployeeService / IGwsrkhService` 接口 + 实现 | +| `Api/Controllers/Prod/KaoqinController.cs` | 追加 `GET/POST/DELETE /cqtj`、`GET /cqtj/edit`、`POST /cqtj/generate` | +| `Api/Controllers/PayRoll/ConfEmployeeController.cs` | `api/payroll/confzq`、`api/payroll/confemployee`、`api/payroll/gwsrkh` | +| `Application/DependencyInjection.cs` | 注册 `IConfZqService / IConfEmployeeService / IGwsrkhService` | +| `Infrastructure/Data/DbSeeder.cs` | 菜单:生产-考勤 增「考勤统计」;薪资 增「账期薪资配置 / 职工薪资配置 / 岗位胜任考核」 | + +### 后端口径(严格对齐老系统) + +- **正常倒挤**:`正常 = 标准 - 请假 - 旷工 - 流动 - 外派 - 代班 - 工伤`;`出勤 = 正常 + 流动 + 代班 + 外派 + 工伤`(不含加班) +- **出勤天数**:`标准 = 工种配置 QqDaysStandard`;`实际 = 本部门出勤时长 / HoursPerday`;`天次 = (出勤+加班)>0 的天数` +- **全勤奖** `quanqin()`(完整移植,含多部门 / 多工种分支): + 1. 总旷工 ≥ 半天 → 0;2. 总出勤 ≤ 3 天 → 0; + 3. 多部门:`∑出勤+带薪 ≥ 15 天` 按实际出勤折算,否则按标准出勤折算;离职 / 新入职分别处理; + 4. 单部门:`∑出勤+带薪请假 ≥ 15 天` 取全额,否则 `全勤奖/标准出勤时长×总出勤`; + 5. 设备办全职:出勤含加班达前缫天数且不低于标准 → 请假不扣; + 6. 其余:`全勤奖 - (全勤奖 / QqHoursMaxQj) × 请假时长` +- **假期补助**:按明细「请假类型 × 出勤工种」分组,病假/轮休/婚假/丧假/法定假/年假/调休分别乘工种单价;`法定假 = 补助小时×JiaqiJineFadingBz + 工资小时×JiaqiJineFadingGz` +- **加班免扣请假**:`出勤+加班 ≥ 前缫出勤天数×HoursPerday 且 加班 ≥ QqHoursDkMinjiaban` → 免扣 `QqHoursDkQingjia` +- **加班费**:按加班明细工种取单价;有岗位胜任考核时按 欠合格/期望/合格/胜任/超胜任 取 `Jiaban_GwsrkhJine*`,无记录取 `JiabanJineBz`;学工加班 = `XgJineGangwei /(QqDaysStandard×HoursPerday) × 时长` +- **流动** = `LiudongJineGuding × 时长`;**代班** = `JineGangwei /(QqDaysStandard×HoursPerday) × 时长`;**夜班** = `JineYeban × 夜班次数`;**旷工考核** = `JiaqiJineKuangongKk × 旷工时长` +- **扎帐校验**:账期 `IsZhaZhang` 时禁止保存 / 删除 / 生成 + +### 前端 + +| 文件 | 说明 | +| --- | --- | +| `views/prod/kaoqin/index.vue`(改) | 补齐工具栏:刷新 / 复制指定日期 / 设标准时长 / 设出勤时长 / **加班 / 流动 / 事假 / 公假 / 代班 / 夜班** / 添加职工 / 删除职工 / 删除整组 / 保存;职工表加多选列;明细表加「出勤部门 / 出勤工种」(代班、流动跨车间);保存前「出勤超标准」二次确认;新增复制日期弹窗(可勾选初始化为正常出勤) | +| `views/prod/kaoqin/cqtj/index.vue`(新) | 考勤统计:年月 + 部门 + 工种 + 关键字;分组表头(出勤天数 / 时长 / 学工 / 请假分项 / 全勤与金额);录入(弹窗内可编辑带「*」字段)、删除、**一键生成**、全部删除;分页 | +| `views/payroll/confzq/index.vue`(新) | 账期薪资配置:年度/月份/前缫出勤天数 | +| `views/payroll/confemployee/index.vue`(新) | 职工薪资配置:职工/工种/法定补助h/法定工资h/学工 | +| `views/payroll/gwsrkh/index.vue`(新) | 岗位胜任考核:年度/月份/部门/职工/考核结果 | +| `router/index.js`(改) | 新增 5 条 componentMap | + +## 四、验证 + +| 项 | 结果 | +| --- | --- | +| 解决方案编译 | **0 个错误** | +| `GET /auth/menus` | ✅ 生产-考勤 → 考勤登记 / 考勤统计;薪资 → …/账期薪资配置 / 职工薪资配置 / 岗位胜任考核 | +| 账期薪资配置 | ✅ POST 2026/10 QsQuanqinDays=26 成功 | +| 考勤登记 | ✅ 46 人保存成功(含 1 人加班 2h + 病假 1h + 夜班 1) | +| 实时汇总 | ✅ `曾红君 出勤=8 加班=2 请假=1 实际天=0.89 天次=1 加班费=30 病假补助=5 补助合计=5 夜班补助=20`(与手算一致:正常 9-1=8,加班费 2×15,病假 1×5,夜班 1×20) | +| 统计保存 / 列表 / 取库 | ✅ 保存 46 → 列表 total=46 → 再录入 `exists=True` | +| 删除 / 一键生成 | ✅ 删除 46;一键生成 1 个车间 | +| 前端 5 个页面 | ✅ vite 全部 200,无 lint | + +测试数据(2026-10-26 的 46 条登记、统计快照、测试用工种薪资配置 3 条、账期配置 1 条)**已全部清理**。 + +## 五、待办 / 已知差异 + +1. **跨部门重复考勤提示**:老系统在加载时提示「X 职工已在 Y 部门录入」,本轮未移植(需额外接口),后续补 +2. **岗位胜任考核**放在【薪资】下(老系统属 `CalXzMain` 薪资模块),仅按年月维护考核结果,未做考核流程 +3. 统计页的**列宽 / 分组表头**在窄屏下需横向滚动,后续可加列配置 +4. 报表设计器脚本 `F9ReportDesignerHandler.ps1` **改完必须确认 UTF-8 BOM 仍在** +5. `views/notification/type/index.vue` 里仍有 `/attendance/approve?leaveId=` 示例文本 + +## 六、下次接续提示 + +- 改后端先按端口 `Stop-Process` 再编译,否则 DLL 锁定;vite 卡死时同样按端口杀掉重启 + (当前:后端 3000 PID 17000,前端 5173 PID 26328) +- 登录 admin / 123456 +- 考勤相关文件: + - 后端 `Domain/Entities/Prod/Cqdj.cs`、`Cqtj.cs`、`Domain/Entities/PayRoll/ConfEmployee.cs` + - `Dtos/Prod/KaoqinDtos.cs`、`Dtos/PayRoll/ConfEmployeeDtos.cs` + - `Services/Prod/KaoqinService.cs`(CQDJ + CQTJ 全部口径)、`Services/PayRoll/ConfEmployeeService.cs` + - `Controllers/Prod/KaoqinController.cs`、`Controllers/PayRoll/ConfEmployeeController.cs` + - 前端 `views/prod/kaoqin/index.vue`、`views/prod/kaoqin/cqtj/index.vue`、 + `views/payroll/confzq|confemployee|gwsrkh/index.vue` +- 老系统参考:`D:\AntaiProject - 副本\trunk\WinClient\F9.Business.Prod\KQ\` +- **每轮对话结尾记得生成本工作进度文档** diff --git a/工作进度/工作进度-27.md b/工作进度/工作进度-27.md new file mode 100644 index 0000000..575bb5b --- /dev/null +++ b/工作进度/工作进度-27.md @@ -0,0 +1,39 @@ +# 工作进度-27 + +> 规则原文:每次对话结尾都要生成工作进度 + +## 本轮任务 + +回答用户提问:**本项目采用的架构是什么**(纯咨询,无代码改动)。 + +## 本轮完成内容 + +1. 梳理并确认项目整体架构: + - 总体:前后端分离 + 单体后端四层分层(DDD + CQRS),IIS 单站点同源部署(`wwwroot` 静态资源 + `MapFallbackToFile("index.html")`)。 + - 后端 .NET 8 四层:`F9WebAiTest.Domain` / `.Application` / `.Infrastructure` / `.Api`,依赖方向 `Api → Application → Domain ← Infrastructure`。 + - CQRS + MediatR 12.4.1(`Application/CQRS/SYS/**/Commands|Queries`,45 个文件),读写仓储分离。 + - 工作单元 `IUnitOfWork_F9`、雪花 ID 单例、FreeSql 软删除全局过滤器。 + - 领域事件 `BusinessChangedEvent` → MediatR Handler → SignalR `/hubs/notification` 推送。 + - RBAC:用户–角色–权限–菜单,JWT Bearer;SignalR 与报表下载走 query string `access_token`。 + - `LongToStringJsonConverter` 解决雪花 ID 精度丢失。 + - 前端 `web/`:Vue 3 + Vite + Element Plus + Pinia + Vue Router + axios,NSwag 生成 `src/api/client.ts`,`v-permission` 按钮级权限,动态路由。 + - 数据层:SQLite + FreeSql(CodeFirst + `DbSeeder` 种子),枚举映射 int 兼容旧库。 + - 遗留/辅助目录:`client/` + `server/`(早期「社区生活圈」Express + Vue3 原型)、`TempleRun/`(旧 WinForms 参考)、`tools/`、`deploy-iis.ps1`、`update-iis.ps1`。 + +## 改动文件 + +无(本轮为架构说明,未修改任何代码)。 + +## 发现的问题(待修正) + +- 根目录 `README.md` 中「数据库:SQLite(EF Core)」的表述已过时,实际 ORM 为 **FreeSql**;README 也缺少 CQRS/MediatR、SignalR、DDD 分层等当前架构描述。 + +## 验证情况 + +- 已通过目录浏览、README、`F9WebAiTest.Api.csproj`、`Program.cs`、`Infrastructure/DependencyInjection.cs`、`web/package.json`、`server/package.json`、`client/package.json` 交叉核对确认。 +- 未编译、未运行(无代码改动)。 + +## 待办 + +1. (可选)更新 `README.md`:EF Core → FreeSql,补充 CQRS/MediatR、SignalR 实时推送、IIS 同源部署说明。 +2. 继续按既有计划推进业务模块迁移。