销售发货

This commit is contained in:
朱昊天
2026-05-18 17:48:43 +08:00
parent f94ddb433d
commit 264ca0e407
59 changed files with 8181 additions and 603 deletions

View File

@@ -0,0 +1,64 @@
package com.gear.oa.controller;
import com.gear.common.annotation.Log;
import com.gear.common.annotation.RepeatSubmit;
import com.gear.common.core.controller.BaseController;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.domain.R;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.core.validate.EditGroup;
import com.gear.common.enums.BusinessType;
import com.gear.oa.domain.bo.GearOrderProductionBo;
import com.gear.oa.domain.vo.GearOrderProductionVo;
import com.gear.oa.service.IGearOrderProductionService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
/**
* 订单生产记录
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/orderProduction")
public class GearOrderProductionController extends BaseController {
private final IGearOrderProductionService iGearOrderProductionService;
@GetMapping("/list")
public TableDataInfo<GearOrderProductionVo> list(GearOrderProductionBo bo, PageQuery pageQuery) {
return iGearOrderProductionService.queryPageList(bo, pageQuery);
}
@GetMapping("/{productionId}")
public R<GearOrderProductionVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long productionId) {
return R.ok(iGearOrderProductionService.queryById(productionId));
}
@Log(title = "订单生产记录", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody GearOrderProductionBo bo) {
return toAjax(iGearOrderProductionService.updateByBo(bo));
}
@Log(title = "订单生产记录", businessType = BusinessType.DELETE)
@DeleteMapping("/{productionIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] productionIds) {
return toAjax(iGearOrderProductionService.deleteWithValidByIds(Arrays.asList(productionIds), true));
}
@Log(title = "订单生产记录", businessType = BusinessType.OTHER)
@RepeatSubmit()
@PostMapping("/init/{orderId}")
public R<Void> init(@NotNull(message = "订单ID不能为空") @PathVariable Long orderId) {
return toAjax(iGearOrderProductionService.initByOrderId(orderId));
}
}

View File

@@ -0,0 +1,102 @@
package com.gear.oa.controller;
import com.gear.common.annotation.Log;
import com.gear.common.annotation.RepeatSubmit;
import com.gear.common.core.controller.BaseController;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.domain.R;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.core.validate.AddGroup;
import com.gear.common.core.validate.EditGroup;
import com.gear.common.enums.BusinessType;
import com.gear.common.utils.poi.ExcelUtil;
import com.gear.oa.domain.bo.GearSalesmanBo;
import com.gear.oa.domain.vo.GearCustomerVo;
import com.gear.oa.domain.vo.GearSalesmanVo;
import com.gear.oa.service.IGearSalesmanService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
/**
* 销售员管理
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/salesman")
public class GearSalesmanController extends BaseController {
private final IGearSalesmanService iGearSalesmanService;
/**
* 查询销售员列表
*/
@GetMapping("/list")
public TableDataInfo<GearSalesmanVo> list(GearSalesmanBo bo, PageQuery pageQuery) {
return iGearSalesmanService.queryPageList(bo, pageQuery);
}
/**
* 导出销售员列表
*/
@Log(title = "销售员", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(GearSalesmanBo bo, HttpServletResponse response) {
List<GearSalesmanVo> list = iGearSalesmanService.queryList(bo);
ExcelUtil.exportExcel(list, "销售员", GearSalesmanVo.class, response);
}
/**
* 获取销售员详情
*/
@GetMapping("/{salesmanId}")
public R<GearSalesmanVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long salesmanId) {
return R.ok(iGearSalesmanService.queryById(salesmanId));
}
/**
* 新增销售员
*/
@Log(title = "销售员", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody GearSalesmanBo bo) {
return toAjax(iGearSalesmanService.insertByBo(bo));
}
/**
* 修改销售员
*/
@Log(title = "销售员", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody GearSalesmanBo bo) {
return toAjax(iGearSalesmanService.updateByBo(bo));
}
/**
* 删除销售员
*/
@Log(title = "销售员", businessType = BusinessType.DELETE)
@DeleteMapping("/{salesmanIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] salesmanIds) {
return toAjax(iGearSalesmanService.deleteWithValidByIds(Arrays.asList(salesmanIds), true));
}
/**
* 跟进客户:按销售员反查客户列表
*/
@GetMapping("/{salesmanId}/customers")
public TableDataInfo<GearCustomerVo> customers(@NotNull(message = "主键不能为空") @PathVariable Long salesmanId,
PageQuery pageQuery) {
return iGearSalesmanService.queryFollowCustomers(salesmanId, pageQuery);
}
}

View File

@@ -0,0 +1,104 @@
package com.gear.oa.controller;
import com.gear.common.annotation.Log;
import com.gear.common.annotation.RepeatSubmit;
import com.gear.common.core.controller.BaseController;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.domain.R;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.core.validate.AddGroup;
import com.gear.common.core.validate.EditGroup;
import com.gear.common.enums.BusinessType;
import com.gear.common.utils.poi.ExcelUtil;
import com.gear.oa.domain.bo.GearShippingOrderBo;
import com.gear.oa.domain.vo.GearShippingOrderVo;
import com.gear.oa.service.IGearShippingOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
/**
* 发货单据
*
* 说明:
* - 本模块写入 gear_shipping_order独立表
* - 用于订单发货/物流信息记录,不与出入库/WMS 绑定
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/shippingOrder")
public class GearShippingOrderController extends BaseController {
private final IGearShippingOrderService iGearShippingOrderService;
/**
* 查询发货单据列表
*/
@GetMapping("/list")
public TableDataInfo<GearShippingOrderVo> list(GearShippingOrderBo bo, PageQuery pageQuery) {
return iGearShippingOrderService.queryPageList(bo, pageQuery);
}
/**
* 根据订单ID查询发货单据列表
*/
@GetMapping("/listByOrderId/{orderId}")
public R<List<GearShippingOrderVo>> listByOrderId(@PathVariable Long orderId) {
return R.ok(iGearShippingOrderService.queryListByOrderId(orderId));
}
/**
* 导出发货单据列表
*/
@Log(title = "发货单据", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(GearShippingOrderBo bo, HttpServletResponse response) {
List<GearShippingOrderVo> list = iGearShippingOrderService.queryList(bo);
ExcelUtil.exportExcel(list, "发货单据", GearShippingOrderVo.class, response);
}
/**
* 获取发货单据详细信息
*/
@GetMapping("/{shippingId}")
public R<GearShippingOrderVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long shippingId) {
return R.ok(iGearShippingOrderService.queryById(shippingId));
}
/**
* 新增发货单据
*/
@Log(title = "发货单据", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody GearShippingOrderBo bo) {
return toAjax(iGearShippingOrderService.insertByBo(bo));
}
/**
* 修改发货单据
*/
@Log(title = "发货单据", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody GearShippingOrderBo bo) {
return toAjax(iGearShippingOrderService.updateByBo(bo));
}
/**
* 删除发货单据
*/
@Log(title = "发货单据", businessType = BusinessType.DELETE)
@DeleteMapping("/{shippingIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] shippingIds) {
return toAjax(iGearShippingOrderService.deleteWithValidByIds(Arrays.asList(shippingIds), true));
}
}

View File

@@ -0,0 +1,100 @@
package com.gear.oa.controller;
import com.gear.common.annotation.Log;
import com.gear.common.annotation.RepeatSubmit;
import com.gear.common.core.controller.BaseController;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.domain.R;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.core.validate.AddGroup;
import com.gear.common.core.validate.EditGroup;
import com.gear.common.enums.BusinessType;
import com.gear.common.utils.poi.ExcelUtil;
import com.gear.oa.domain.bo.GearShippingPlanBo;
import com.gear.oa.domain.vo.GearShippingPlanVo;
import com.gear.oa.service.IGearShippingPlanService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
/**
* 发货计划
*/
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/oa/shippingPlan")
public class GearShippingPlanController extends BaseController {
private final IGearShippingPlanService iGearShippingPlanService;
/**
* 查询发货计划列表
*/
@GetMapping("/list")
public TableDataInfo<GearShippingPlanVo> list(GearShippingPlanBo bo, PageQuery pageQuery) {
return iGearShippingPlanService.queryPageList(bo, pageQuery);
}
/**
* 查询发货计划列表(带单据数)
*/
@GetMapping("/listWithCount")
public R<List<GearShippingPlanVo>> listWithCount(GearShippingPlanBo bo) {
return R.ok(iGearShippingPlanService.queryListWithCount(bo));
}
/**
* 导出发货计划列表
*/
@Log(title = "发货计划", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(GearShippingPlanBo bo, HttpServletResponse response) {
List<GearShippingPlanVo> list = iGearShippingPlanService.queryList(bo);
ExcelUtil.exportExcel(list, "发货计划", GearShippingPlanVo.class, response);
}
/**
* 获取发货计划详细信息
*/
@GetMapping("/{planId}")
public R<GearShippingPlanVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long planId) {
return R.ok(iGearShippingPlanService.queryById(planId));
}
/**
* 新增发货计划
*/
@Log(title = "发货计划", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody GearShippingPlanBo bo) {
return toAjax(iGearShippingPlanService.insertByBo(bo));
}
/**
* 修改发货计划
*/
@Log(title = "发货计划", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody GearShippingPlanBo bo) {
return toAjax(iGearShippingPlanService.updateByBo(bo));
}
/**
* 删除发货计划
*/
@Log(title = "发货计划", businessType = BusinessType.DELETE)
@DeleteMapping("/{planIds}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] planIds) {
return toAjax(iGearShippingPlanService.deleteWithValidByIds(Arrays.asList(planIds), true));
}
}

View File

@@ -22,6 +22,8 @@ import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.math.BigDecimal;
/**
* 库存:原材料/产品与库区/库位的存放关系
@@ -45,6 +47,12 @@ public class GearStockController extends BaseController {
return iGearStockService.queryPageList(bo, pageQuery);
}
@PostMapping("/sumQuantityByItemIds")
public R<Map<Long, BigDecimal>> sumQuantityByItemIds(@RequestParam(defaultValue = "product") String itemType,
@RequestBody List<Long> itemIds) {
return R.ok(iGearStockService.sumQuantityByItemIds(itemType, itemIds));
}
/**
* 导出库存:原材料/产品与库区/库位的存放关系列表
*/

View File

@@ -44,6 +44,12 @@ public class GearOrder extends BaseEntity {
* 销售经理
*/
private String salesManager;
/**
* 销售员ID挂接销售员管理
*/
@TableField("salesman_id")
private Long salesmanId;
/**
* 订单状态0=新建1=生产中2=已完成3=已取消)
*/
@@ -70,4 +76,10 @@ public class GearOrder extends BaseEntity {
*/
private BigDecimal noTaxAmount;
/**
* 合同Excel附件ossId列表逗号分隔
*/
@TableField("contract_excel_oss_ids")
private String contractExcelOssIds;
}

View File

@@ -0,0 +1,44 @@
package com.gear.oa.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.gear.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 订单生产记录对象 gear_order_production
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("gear_order_production")
public class GearOrderProduction extends BaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "production_id")
private Long productionId;
private Long orderId;
private Long orderDetailId;
private Long productId;
private BigDecimal planQty;
private BigDecimal finishedQty;
private BigDecimal badQty;
private String unit;
private String remark;
@TableLogic(value = "0", delval = "2")
private String delFlag;
}

View File

@@ -0,0 +1,52 @@
package com.gear.oa.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.gear.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 销售员对象 gear_salesman
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("gear_salesman")
public class GearSalesman extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 销售员ID
*/
@TableId(value = "salesman_id")
private Long salesmanId;
/**
* 销售员姓名
*/
private String name;
/**
* 联系电话
*/
private String mobile;
/**
* 状态0正常 1停用
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 删除标志0=正常1=已删除)
*/
@TableLogic
private Integer delFlag;
}

View File

@@ -0,0 +1,117 @@
package com.gear.oa.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.gear.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 发货单据对象 gear_shipping_order
*
* 说明:
* - 用于记录订单发货信息(物流公司/运单号/收货信息等)
* - 与出入库/WMS 单据无关,避免与库存模块耦合
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("gear_shipping_order")
public class GearShippingOrder extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 发货单据ID
*/
@TableId(value = "shipping_id")
private Long shippingId;
/**
* 发货单号(唯一)
*/
private String shippingNo;
/**
* 发货单名称(展示用)
*/
private String shippingName;
/**
* 发货计划ID
*/
private Long planId;
/**
* 关联订单ID
*/
private Long orderId;
/**
* 订单编号快照
*/
private String orderCode;
/**
* 收货单位
*/
private String receiverCompany;
/**
* 收货客户ID真实归属客户
*/
private Long receiverCustomerId;
/**
* 发货时间
*/
private Date shipTime;
/**
* 负责人
*/
private String responsibleName;
/**
* 物流公司
*/
private String logisticsCompany;
/**
* 运单号
*/
private String logisticsNo;
/**
* 收货人
*/
private String receiverName;
/**
* 收货电话
*/
private String receiverPhone;
/**
* 收货地址
*/
private String receiverAddress;
/**
* 完成状态0未发货 1已打印 2已发货 3已完成
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 删除标志0存在 2删除
*/
@TableLogic(value = "0", delval = "2")
private String delFlag;
}

View File

@@ -0,0 +1,55 @@
package com.gear.oa.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.gear.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 发货计划对象 gear_shipping_plan
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("gear_shipping_plan")
public class GearShippingPlan extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 发货计划ID
*/
@TableId(value = "plan_id")
private Long planId;
/**
* 计划名称
*/
private String planName;
/**
* 计划日期
*/
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date planDate;
/**
* 状态0未完成 1已完成
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 删除标志0存在 2删除
*/
@TableLogic(value = "0", delval = "2")
private String delFlag;
}

View File

@@ -48,6 +48,11 @@ public class GearOrderBo extends BaseEntity {
*/
private String salesManager;
/**
* 销售员ID挂接销售员管理
*/
private Long salesmanId;
/**
* 订单状态0=新建1=生产中2=已完成3=已取消)
*/
@@ -73,5 +78,9 @@ public class GearOrderBo extends BaseEntity {
*/
private BigDecimal noTaxAmount;
/**
* 合同Excel附件ossId列表逗号分隔
*/
private String contractExcelOssIds;
}

View File

@@ -0,0 +1,37 @@
package com.gear.oa.domain.bo;
import com.gear.common.core.domain.BaseEntity;
import com.gear.common.core.validate.EditGroup;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.math.BigDecimal;
/**
* 订单生产记录业务对象 gear_order_production
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GearOrderProductionBo extends BaseEntity {
private Long productionId;
private Long orderId;
private Long orderDetailId;
private Long productId;
private BigDecimal planQty;
@NotNull(message = "完成数量不能为空", groups = {EditGroup.class})
private BigDecimal finishedQty;
private BigDecimal badQty;
private String unit;
private String remark;
}

View File

@@ -0,0 +1,39 @@
package com.gear.oa.domain.bo;
import com.gear.common.core.domain.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 销售员业务对象 gear_salesman
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GearSalesmanBo extends BaseEntity {
/**
* 销售员ID
*/
private Long salesmanId;
/**
* 销售员姓名
*/
private String name;
/**
* 联系电话
*/
private String mobile;
/**
* 状态0正常 1停用
*/
private Integer status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,138 @@
package com.gear.oa.domain.bo;
import com.gear.common.core.domain.BaseEntity;
import com.gear.common.core.validate.AddGroup;
import com.gear.common.core.validate.EditGroup;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
/**
* 发货单据业务对象 gear_shipping_order
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GearShippingOrderBo extends BaseEntity {
/**
* 发货单据ID
*/
private Long shippingId;
/**
* 发货单号(唯一)
*/
private String shippingNo;
/**
* 发货单名称(展示用)
*/
private String shippingName;
/**
* 发货计划ID
*/
private Long planId;
/**
* 关联订单ID
*
* 说明:
* - 发货单据必须绑定订单(订单是发货的业务来源)
* - 前端存在“只修改完成状态”的场景(仅提交 shippingId/status这里仍要求携带 orderId
* 便于做订单状态联动与数据归属校验
*/
@NotNull(message = "订单ID不能为空", groups = {AddGroup.class, EditGroup.class})
private Long orderId;
/**
* 客户ID用于按客户维度查询发货单据通过订单反查
*
* 说明:发货单据表本身不存 customerId这里是查询条件后端会先查出订单ID集合再过滤发货单据
*/
private Long customerId;
/**
* 销售员ID用于按销售员维度查询发货单据通过订单反查
*
* 说明:发货单据表本身不存 salesmanId这里是查询条件后端会先查出订单ID集合再过滤发货单据
*/
private Long salesmanId;
/**
* 订单编号快照
*/
private String orderCode;
/**
* 收货单位
*/
private String receiverCompany;
/**
* 收货客户ID真实归属客户
*
* 说明:用于客户维度归属统计/查询(客户管理页按此字段精确查询“该客户实际收货”的发货单据)
*/
private Long receiverCustomerId;
/**
* 发货时间
*/
private Date shipTime;
/**
* 发货时间范围(开始)
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date shipTimeStart;
/**
* 发货时间范围(结束)
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date shipTimeEnd;
/**
* 负责人
*/
private String responsibleName;
/**
* 物流公司
*/
private String logisticsCompany;
/**
* 运单号
*/
private String logisticsNo;
/**
* 收货人
*/
private String receiverName;
/**
* 收货电话
*/
private String receiverPhone;
/**
* 收货地址
*/
private String receiverAddress;
/**
* 完成状态0未发货 1已打印 2已发货 3已完成
*/
private String status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,50 @@
package com.gear.oa.domain.bo;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.gear.common.core.domain.BaseEntity;
import com.gear.common.core.validate.AddGroup;
import com.gear.common.core.validate.EditGroup;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* 发货计划业务对象 gear_shipping_plan
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GearShippingPlanBo extends BaseEntity {
/**
* 发货计划ID
*/
private Long planId;
/**
* 计划名称
*/
@NotBlank(message = "计划名称不能为空", groups = {AddGroup.class, EditGroup.class})
private String planName;
/**
* 计划日期
*/
@NotNull(message = "计划日期不能为空", groups = {AddGroup.class, EditGroup.class})
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date planDate;
/**
* 状态0未完成 1已完成
*/
private String status;
/**
* 备注
*/
private String remark;
}

View File

@@ -76,4 +76,25 @@ public class GearOrderDetailVo {
private String productName;
private String productCode;
/**
* 产品类型product=产品/成品semi=半成品raw=原料)
* 说明:用于“订单明细”页过滤,只允许成品进入发货
*/
private String productType;
/**
* 产品规格(来自产品管理 mat_product
*/
private String spec;
/**
* 产品型号(来自产品管理 mat_product
*/
private String model;
/**
* 产品单价(来自产品管理 mat_product用于订单明细快捷填写
*/
private BigDecimal unitPrice;
}

View File

@@ -0,0 +1,46 @@
package com.gear.oa.domain.vo;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
/**
* 订单生产记录视图对象 gear_order_production
*/
@Data
public class GearOrderProductionVo {
private Long productionId;
private Long orderId;
private Long orderDetailId;
private Long productId;
private BigDecimal planQty;
private BigDecimal finishedQty;
private BigDecimal badQty;
private String unit;
private String remark;
private Date createTime;
private Date updateTime;
private String productName;
private String productCode;
private String productType;
private String spec;
private String model;
}

View File

@@ -50,11 +50,16 @@ public class GearOrderVo extends BaseEntity {
private Integer company;
/**
* 销售经理
* 销售员(展示用:从 gear_salesman.name 联查得到)
*/
@ExcelProperty(value = "销售经理")
@ExcelProperty(value = "销售")
private String salesManager;
/**
* 销售员ID挂接销售员管理
*/
private Long salesmanId;
/**
* 订单状态0=新建1=生产中2=已完成3=已取消)
*/
@@ -86,6 +91,18 @@ public class GearOrderVo extends BaseEntity {
@ExcelProperty(value = "无税金额")
private BigDecimal noTaxAmount;
/**
* 合同Excel附件ossId列表逗号分隔
*/
private String contractExcelOssIds;
private String customerName;
private Integer shippingMaxStatus;
private Integer shippedFlag;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastShipTime;
}

View File

@@ -0,0 +1,31 @@
package com.gear.oa.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
/**
* 销售员视图对象 gear_salesman
*/
@Data
@ExcelIgnoreUnannotated
public class GearSalesmanVo {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "销售员ID")
private Long salesmanId;
@ExcelProperty(value = "销售员姓名")
private String name;
@ExcelProperty(value = "联系电话")
private String mobile;
@ExcelProperty(value = "状态0正常 1停用")
private Integer status;
@ExcelProperty(value = "备注")
private String remark;
}

View File

@@ -0,0 +1,69 @@
package com.gear.oa.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.gear.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* 发货单据视图对象 gear_shipping_order
*/
@Data
@ExcelIgnoreUnannotated
public class GearShippingOrderVo extends BaseEntity {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "发货单据ID")
private Long shippingId;
@ExcelProperty(value = "发货单号")
private String shippingNo;
@ExcelProperty(value = "发货单名称")
private String shippingName;
@ExcelProperty(value = "发货计划ID")
private Long planId;
@ExcelProperty(value = "订单ID")
private Long orderId;
@ExcelProperty(value = "订单编号")
private String orderCode;
@ExcelProperty(value = "收货单位")
private String receiverCompany;
@ExcelProperty(value = "收货客户ID")
private Long receiverCustomerId;
@ExcelProperty(value = "发货时间")
private Date shipTime;
@ExcelProperty(value = "负责人")
private String responsibleName;
@ExcelProperty(value = "物流公司")
private String logisticsCompany;
@ExcelProperty(value = "运单号")
private String logisticsNo;
@ExcelProperty(value = "收货人")
private String receiverName;
@ExcelProperty(value = "收货电话")
private String receiverPhone;
@ExcelProperty(value = "收货地址")
private String receiverAddress;
@ExcelProperty(value = "完成状态")
private String status;
@ExcelProperty(value = "备注")
private String remark;
}

View File

@@ -0,0 +1,37 @@
package com.gear.oa.domain.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import com.gear.common.core.domain.BaseEntity;
import lombok.Data;
import java.util.Date;
/**
* 发货计划视图对象 gear_shipping_plan
*/
@Data
@ExcelIgnoreUnannotated
public class GearShippingPlanVo extends BaseEntity {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = "计划ID")
private Long planId;
@ExcelProperty(value = "计划名称")
private String planName;
@ExcelProperty(value = "计划日期")
private Date planDate;
@ExcelProperty(value = "状态")
private String status;
@ExcelProperty(value = "备注")
private String remark;
@ExcelProperty(value = "单据数")
private Long shippingCount;
}

View File

@@ -0,0 +1,20 @@
package com.gear.oa.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gear.common.core.mapper.BaseMapperPlus;
import com.gear.oa.domain.GearOrderProduction;
import com.gear.oa.domain.vo.GearOrderProductionVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 订单生产记录Mapper接口
*/
public interface GearOrderProductionMapper extends BaseMapperPlus<GearOrderProductionMapper, GearOrderProduction, GearOrderProductionVo> {
Page<GearOrderProductionVo> selectVoPagePlus(Page<?> page, @Param("ew") Wrapper<GearOrderProduction> wrapper);
List<GearOrderProductionVo> selectVoListPlus(@Param("ew") Wrapper<GearOrderProduction> wrapper);
}

View File

@@ -0,0 +1,12 @@
package com.gear.oa.mapper;
import com.gear.common.core.mapper.BaseMapperPlus;
import com.gear.oa.domain.GearSalesman;
import com.gear.oa.domain.vo.GearSalesmanVo;
/**
* 销售员Mapper接口
*/
public interface GearSalesmanMapper extends BaseMapperPlus<GearSalesmanMapper, GearSalesman, GearSalesmanVo> {
}

View File

@@ -0,0 +1,12 @@
package com.gear.oa.mapper;
import com.gear.common.core.mapper.BaseMapperPlus;
import com.gear.oa.domain.GearShippingOrder;
import com.gear.oa.domain.vo.GearShippingOrderVo;
/**
* 发货单据Mapper
*/
public interface GearShippingOrderMapper extends BaseMapperPlus<GearShippingOrderMapper, GearShippingOrder, GearShippingOrderVo> {
}

View File

@@ -0,0 +1,50 @@
package com.gear.oa.mapper;
import com.gear.common.core.mapper.BaseMapperPlus;
import com.gear.oa.domain.GearShippingPlan;
import com.gear.oa.domain.bo.GearShippingPlanBo;
import com.gear.oa.domain.vo.GearShippingPlanVo;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 发货计划Mapper
*/
public interface GearShippingPlanMapper extends BaseMapperPlus<GearShippingPlanMapper, GearShippingPlan, GearShippingPlanVo> {
@Select({
"<script>",
"SELECT",
" p.plan_id,",
" p.plan_name,",
" p.plan_date,",
" p.status,",
" p.remark,",
" p.create_by,",
" p.create_time,",
" p.update_by,",
" p.update_time,",
" COUNT(o.shipping_id) AS shipping_count",
"FROM gear_shipping_plan p",
"LEFT JOIN gear_shipping_order o",
" ON o.plan_id = p.plan_id",
" AND o.del_flag = '0'",
"WHERE p.del_flag = '0'",
"<if test='bo != null and bo.planName != null and bo.planName != \"\"'>",
" AND p.plan_name LIKE CONCAT('%', #{bo.planName}, '%')",
"</if>",
"<if test='bo != null and bo.status != null and bo.status != \"\"'>",
" AND p.status = #{bo.status}",
"</if>",
"<if test='bo != null and bo.planDate != null'>",
" AND p.plan_date = #{bo.planDate}",
"</if>",
"GROUP BY p.plan_id",
"ORDER BY p.plan_date DESC, p.create_time DESC",
"</script>"
})
List<GearShippingPlanVo> selectVoListWithCount(@Param("bo") GearShippingPlanBo bo);
}

View File

@@ -8,6 +8,8 @@ import com.gear.oa.domain.vo.GearStockVo;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 库存:原材料/产品与库区/库位的存放关系Mapper接口
@@ -19,6 +21,8 @@ public interface GearStockMapper extends BaseMapperPlus<GearStockMapper, GearSto
BigDecimal getStockByItemId(Long rawMaterialId);
List<Map<String, Object>> selectSumQuantityByItemIds(@Param("itemType") String itemType, @Param("itemIds") List<Long> itemIds);
/**
* 分页联查物品名称和编码支持Wrapper动态条件返回Page<GearStockVo>
*/

View File

@@ -0,0 +1,31 @@
package com.gear.oa.service;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.oa.domain.bo.GearOrderProductionBo;
import com.gear.oa.domain.vo.GearOrderProductionVo;
import java.util.Collection;
import java.util.List;
/**
* 订单生产记录Service接口
*/
public interface IGearOrderProductionService {
GearOrderProductionVo queryById(Long productionId);
TableDataInfo<GearOrderProductionVo> queryPageList(GearOrderProductionBo bo, PageQuery pageQuery);
List<GearOrderProductionVo> queryList(GearOrderProductionBo bo);
Boolean updateByBo(GearOrderProductionBo bo);
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 初始化/同步:按订单明细生成生产记录,并同步计划数量
*/
Boolean initByOrderId(Long orderId);
}

View File

@@ -0,0 +1,34 @@
package com.gear.oa.service;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.oa.domain.bo.GearSalesmanBo;
import com.gear.oa.domain.vo.GearCustomerVo;
import com.gear.oa.domain.vo.GearSalesmanVo;
import java.util.Collection;
import java.util.List;
/**
* 销售员Service接口
*/
public interface IGearSalesmanService {
GearSalesmanVo queryById(Long salesmanId);
TableDataInfo<GearSalesmanVo> queryPageList(GearSalesmanBo bo, PageQuery pageQuery);
List<GearSalesmanVo> queryList(GearSalesmanBo bo);
Boolean insertByBo(GearSalesmanBo bo);
Boolean updateByBo(GearSalesmanBo bo);
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 查询销售员的跟进客户从订单中反查salesManager=销售员姓名 -> customerId -> 客户信息)
*/
TableDataInfo<GearCustomerVo> queryFollowCustomers(Long salesmanId, PageQuery pageQuery);
}

View File

@@ -0,0 +1,51 @@
package com.gear.oa.service;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.oa.domain.bo.GearShippingOrderBo;
import com.gear.oa.domain.vo.GearShippingOrderVo;
import java.util.Collection;
import java.util.List;
/**
* 发货单据Service接口
*/
public interface IGearShippingOrderService {
/**
* 查询发货单据
*/
GearShippingOrderVo queryById(Long shippingId);
/**
* 查询发货单据列表(分页)
*/
TableDataInfo<GearShippingOrderVo> queryPageList(GearShippingOrderBo bo, PageQuery pageQuery);
/**
* 查询发货单据列表(不分页)
*/
List<GearShippingOrderVo> queryList(GearShippingOrderBo bo);
/**
* 新增发货单据
*/
Boolean insertByBo(GearShippingOrderBo bo);
/**
* 修改发货单据
*/
Boolean updateByBo(GearShippingOrderBo bo);
/**
* 删除发货单据
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 根据订单ID查询发货单据
*/
List<GearShippingOrderVo> queryListByOrderId(Long orderId);
}

View File

@@ -0,0 +1,30 @@
package com.gear.oa.service;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.oa.domain.bo.GearShippingPlanBo;
import com.gear.oa.domain.vo.GearShippingPlanVo;
import java.util.Collection;
import java.util.List;
/**
* 发货计划Service接口
*/
public interface IGearShippingPlanService {
GearShippingPlanVo queryById(Long planId);
TableDataInfo<GearShippingPlanVo> queryPageList(GearShippingPlanBo bo, PageQuery pageQuery);
List<GearShippingPlanVo> queryList(GearShippingPlanBo bo);
List<GearShippingPlanVo> queryListWithCount(GearShippingPlanBo bo);
Boolean insertByBo(GearShippingPlanBo bo);
Boolean updateByBo(GearShippingPlanBo bo);
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
}

View File

@@ -8,6 +8,7 @@ import com.gear.oa.domain.vo.GearStockVo;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 库存:原材料/产品与库区/库位的存放关系Service接口
@@ -47,6 +48,8 @@ public interface IGearStockService {
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
Map<Long, BigDecimal> sumQuantityByItemIds(String itemType, List<Long> itemIds);
// /**
// * 根据原材料ID获取库存数量 (用于生成推荐采购计划)
// */

View File

@@ -0,0 +1,150 @@
package com.gear.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.utils.StringUtils;
import com.gear.oa.domain.GearOrderProduction;
import com.gear.oa.domain.bo.GearOrderProductionBo;
import com.gear.oa.domain.vo.GearOrderDetailVo;
import com.gear.oa.domain.vo.GearOrderProductionVo;
import com.gear.oa.mapper.GearOrderDetailMapper;
import com.gear.oa.mapper.GearOrderProductionMapper;
import com.gear.oa.service.IGearOrderProductionService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 订单生产记录Service业务层处理
*/
@RequiredArgsConstructor
@Service
public class GearOrderProductionServiceImpl implements IGearOrderProductionService {
private final GearOrderProductionMapper baseMapper;
private final GearOrderDetailMapper orderDetailMapper;
@Override
public GearOrderProductionVo queryById(Long productionId) {
return baseMapper.selectVoById(productionId);
}
@Override
public TableDataInfo<GearOrderProductionVo> queryPageList(GearOrderProductionBo bo, PageQuery pageQuery) {
QueryWrapper<GearOrderProduction> qw = buildQueryWrapperPlus(bo);
Page<GearOrderProductionVo> result = baseMapper.selectVoPagePlus(pageQuery.build(), qw);
return TableDataInfo.build(result);
}
@Override
public List<GearOrderProductionVo> queryList(GearOrderProductionBo bo) {
QueryWrapper<GearOrderProduction> qw = buildQueryWrapperPlus(bo);
return baseMapper.selectVoListPlus(qw);
}
private QueryWrapper<GearOrderProduction> buildQueryWrapperPlus(GearOrderProductionBo bo) {
Map<String, Object> params = bo.getParams();
QueryWrapper<GearOrderProduction> qw = Wrappers.query();
qw.eq("p.del_flag", "0");
qw.eq(bo.getProductionId() != null, "p.production_id", bo.getProductionId());
qw.eq(bo.getOrderId() != null, "p.order_id", bo.getOrderId());
qw.eq(bo.getOrderDetailId() != null, "p.order_detail_id", bo.getOrderDetailId());
qw.eq(bo.getProductId() != null, "p.product_id", bo.getProductId());
qw.orderByAsc("p.order_detail_id");
qw.orderByDesc("p.update_time");
qw.orderByDesc("p.create_time");
return qw;
}
@Override
public Boolean updateByBo(GearOrderProductionBo bo) {
GearOrderProduction update = BeanUtil.toBean(bo, GearOrderProduction.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
private void validEntityBeforeSave(GearOrderProduction entity) {
if (entity == null) return;
if (entity.getProductionId() == null) {
entity.setProductionId(IdUtil.getSnowflakeNextId());
}
if (entity.getFinishedQty() == null) {
entity.setFinishedQty(BigDecimal.ZERO);
}
if (entity.getBadQty() == null) {
entity.setBadQty(BigDecimal.ZERO);
}
if (StringUtils.isBlank(entity.getDelFlag())) {
entity.setDelFlag("0");
}
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public Boolean initByOrderId(Long orderId) {
if (orderId == null) return false;
List<GearOrderDetailVo> details = orderDetailMapper.selectVoListByOrderId(orderId);
if (details == null || details.isEmpty()) return true;
List<Long> detailIds = details.stream().map(GearOrderDetailVo::getDetailId).filter(Objects::nonNull).distinct().collect(Collectors.toList());
if (detailIds.isEmpty()) return true;
List<GearOrderProduction> existing = baseMapper.selectList(Wrappers.<GearOrderProduction>lambdaQuery()
.in(GearOrderProduction::getOrderDetailId, detailIds));
Map<Long, GearOrderProduction> existMap = new HashMap<>();
if (existing != null) {
for (GearOrderProduction e : existing) {
if (e != null && e.getOrderDetailId() != null) {
existMap.put(e.getOrderDetailId(), e);
}
}
}
for (GearOrderDetailVo d : details) {
if (d == null || d.getDetailId() == null) continue;
GearOrderProduction hit = existMap.get(d.getDetailId());
BigDecimal planQty = d.getQuantity() == null ? BigDecimal.ZERO : BigDecimal.valueOf(d.getQuantity());
if (hit == null) {
GearOrderProduction add = new GearOrderProduction();
add.setProductionId(IdUtil.getSnowflakeNextId());
add.setOrderId(orderId);
add.setOrderDetailId(d.getDetailId());
add.setProductId(d.getProductId());
add.setPlanQty(planQty);
add.setFinishedQty(BigDecimal.ZERO);
add.setBadQty(BigDecimal.ZERO);
add.setUnit(d.getUnit() == null ? "" : d.getUnit());
add.setDelFlag("0");
baseMapper.insert(add);
} else {
GearOrderProduction upd = new GearOrderProduction();
upd.setProductionId(hit.getProductionId());
upd.setOrderId(orderId);
upd.setOrderDetailId(d.getDetailId());
upd.setProductId(d.getProductId());
upd.setPlanQty(planQty);
upd.setUnit(d.getUnit() == null ? "" : d.getUnit());
baseMapper.updateById(upd);
}
}
return true;
}
}

View File

@@ -13,7 +13,9 @@ import org.springframework.stereotype.Service;
import com.gear.oa.domain.bo.GearOrderBo;
import com.gear.oa.domain.vo.GearOrderVo;
import com.gear.oa.domain.GearOrder;
import com.gear.oa.domain.GearShippingOrder;
import com.gear.oa.mapper.GearOrderMapper;
import com.gear.oa.mapper.GearShippingOrderMapper;
import com.gear.oa.service.IGearOrderService;
import java.util.List;
@@ -31,13 +33,51 @@ import java.util.Collection;
public class GearOrderServiceImpl implements IGearOrderService {
private final GearOrderMapper baseMapper;
private final GearShippingOrderMapper shippingOrderMapper;
/**
* 查询订单主
*/
@Override
public GearOrderVo queryById(Long orderId){
return baseMapper.selectVoById(orderId);
GearOrderVo vo = baseMapper.selectVoById(orderId);
if (vo == null) {
return null;
}
Integer derived = deriveOrderStatusByShipping(orderId, vo.getOrderStatus());
if (derived != null && vo.getOrderStatus() != null && !derived.equals(vo.getOrderStatus())) {
baseMapper.update(null, Wrappers.<GearOrder>lambdaUpdate()
.eq(GearOrder::getOrderId, orderId)
.ne(GearOrder::getOrderStatus, 3)
.in(GearOrder::getOrderStatus, 0, 1)
.set(GearOrder::getOrderStatus, derived));
vo.setOrderStatus(derived);
}
return vo;
}
private Integer deriveOrderStatusByShipping(Long orderId, Integer currentStatus) {
if (orderId == null) return currentStatus;
if (currentStatus != null && currentStatus == 3) return 3;
QueryWrapper<GearShippingOrder> qw = new QueryWrapper<>();
qw.select("MAX(CAST(status AS SIGNED))");
qw.eq("order_id", orderId);
qw.eq("del_flag", "0");
List<Object> objs = shippingOrderMapper.selectObjs(qw);
Integer maxStatus = null;
if (objs != null && !objs.isEmpty() && objs.get(0) != null) {
try {
maxStatus = Integer.parseInt(String.valueOf(objs.get(0)));
} catch (Exception ignored) {
maxStatus = null;
}
}
int st = currentStatus == null ? 0 : currentStatus;
if (maxStatus != null && maxStatus >= 3 && (st == 0 || st == 1)) return 2;
if (maxStatus != null && maxStatus >= 2 && st == 0) return 1;
return currentStatus;
}
/**
@@ -55,8 +95,11 @@ public class GearOrderServiceImpl implements IGearOrderService {
//表别名的方式进行联查
lqw.like(StringUtils.isNotBlank(bo.getOrderCode()), "o.order_code", bo.getOrderCode());
lqw.eq(bo.getCustomerId() != null, "o.customer_id", bo.getCustomerId());
lqw.like(StringUtils.isNotBlank(bo.getSalesManager()), "o.sales_manager", bo.getSalesManager());
lqw.eq(bo.getSalesmanId() != null, "o.salesman_id", bo.getSalesmanId());
lqw.like(StringUtils.isNotBlank(bo.getSalesManager()), "s.name", bo.getSalesManager());
lqw.eq(bo.getOrderStatus() != null, "o.order_status", bo.getOrderStatus());
lqw.apply(bo.getOrderStatus() != null && bo.getOrderStatus() == 0,
"NOT EXISTS (SELECT 1 FROM gear_shipping_order so WHERE so.order_id = o.order_id AND so.del_flag = '0' AND CAST(so.status AS SIGNED) >= 2)");
lqw.eq(bo.getTradeType() != null, "o.trade_type", bo.getTradeType());
lqw.eq(bo.getTaxAmount() != null, "o.tax_amount", bo.getTaxAmount());
lqw.eq(bo.getNoTaxAmount() != null, "o.no_tax_amount", bo.getNoTaxAmount());
@@ -78,6 +121,7 @@ public class GearOrderServiceImpl implements IGearOrderService {
LambdaQueryWrapper<GearOrder> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getOrderCode()), GearOrder::getOrderCode, bo.getOrderCode());
lqw.eq(bo.getCustomerId() != null, GearOrder::getCustomerId, bo.getCustomerId());
lqw.eq(bo.getSalesmanId() != null, GearOrder::getSalesmanId, bo.getSalesmanId());
lqw.eq(StringUtils.isNotBlank(bo.getSalesManager()), GearOrder::getSalesManager, bo.getSalesManager());
lqw.eq(bo.getOrderStatus() != null, GearOrder::getOrderStatus, bo.getOrderStatus());
lqw.eq(bo.getTradeType() != null, GearOrder::getTradeType, bo.getTradeType());

View File

@@ -0,0 +1,131 @@
package com.gear.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.utils.StringUtils;
import com.gear.oa.domain.GearCustomer;
import com.gear.oa.domain.GearOrder;
import com.gear.oa.domain.GearSalesman;
import com.gear.oa.domain.bo.GearSalesmanBo;
import com.gear.oa.domain.vo.GearCustomerVo;
import com.gear.oa.domain.vo.GearSalesmanVo;
import com.gear.oa.mapper.GearCustomerMapper;
import com.gear.oa.mapper.GearOrderMapper;
import com.gear.oa.mapper.GearSalesmanMapper;
import com.gear.oa.service.IGearSalesmanService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 销售员Service业务层处理
*/
@RequiredArgsConstructor
@Service
public class GearSalesmanServiceImpl implements IGearSalesmanService {
private final GearSalesmanMapper baseMapper;
private final GearOrderMapper gearOrderMapper;
private final GearCustomerMapper gearCustomerMapper;
@Override
public GearSalesmanVo queryById(Long salesmanId) {
return baseMapper.selectVoById(salesmanId);
}
@Override
public TableDataInfo<GearSalesmanVo> queryPageList(GearSalesmanBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<GearSalesman> lqw = buildQueryWrapper(bo);
Page<GearSalesmanVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
@Override
public List<GearSalesmanVo> queryList(GearSalesmanBo bo) {
LambdaQueryWrapper<GearSalesman> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<GearSalesman> buildQueryWrapper(GearSalesmanBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<GearSalesman> lqw = Wrappers.lambdaQuery();
lqw.like(StringUtils.isNotBlank(bo.getName()), GearSalesman::getName, bo.getName());
lqw.eq(StringUtils.isNotBlank(bo.getMobile()), GearSalesman::getMobile, bo.getMobile());
lqw.eq(bo.getStatus() != null, GearSalesman::getStatus, bo.getStatus());
return lqw;
}
@Override
public Boolean insertByBo(GearSalesmanBo bo) {
GearSalesman add = BeanUtil.toBean(bo, GearSalesman.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setSalesmanId(add.getSalesmanId());
}
return flag;
}
@Override
public Boolean updateByBo(GearSalesmanBo bo) {
GearSalesman update = BeanUtil.toBean(bo, GearSalesman.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
private void validEntityBeforeSave(GearSalesman entity) {
// TODO 可在这里增加“姓名唯一”等约束校验
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// TODO 可在这里增加“存在关联订单/客户则不允许删除”等业务校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public TableDataInfo<GearCustomerVo> queryFollowCustomers(Long salesmanId, PageQuery pageQuery) {
GearSalesman salesman = baseMapper.selectById(salesmanId);
if (salesman == null) {
return TableDataInfo.build(new Page<>());
}
// 说明:销售经理已挂接销售员,这里通过 salesman_id 反查客户
LambdaQueryWrapper<GearOrder> orderQw = Wrappers.lambdaQuery();
orderQw.select(GearOrder::getCustomerId);
orderQw.eq(GearOrder::getSalesmanId, salesmanId);
orderQw.eq(GearOrder::getDelFlag, 0);
orderQw.isNotNull(GearOrder::getCustomerId);
orderQw.groupBy(GearOrder::getCustomerId);
List<GearOrder> orders = gearOrderMapper.selectList(orderQw);
Set<Long> customerIds = orders.stream()
.map(GearOrder::getCustomerId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (customerIds.isEmpty()) {
return TableDataInfo.build(new Page<>());
}
// 说明:客户表有逻辑删除标志,这里仅取未删除的数据
LambdaQueryWrapper<GearCustomer> customerQw = Wrappers.lambdaQuery();
customerQw.in(GearCustomer::getCustomerId, customerIds);
customerQw.eq(GearCustomer::getDelFlag, 0);
Page<GearCustomerVo> page = gearCustomerMapper.selectVoPage(pageQuery.build(), customerQw);
return TableDataInfo.build(page);
}
}

View File

@@ -0,0 +1,195 @@
package com.gear.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.utils.StringUtils;
import com.gear.oa.domain.GearOrder;
import com.gear.oa.domain.GearShippingOrder;
import com.gear.oa.domain.bo.GearShippingOrderBo;
import com.gear.oa.domain.vo.GearShippingOrderVo;
import com.gear.oa.mapper.GearOrderMapper;
import com.gear.oa.mapper.GearShippingOrderMapper;
import com.gear.oa.service.IGearShippingOrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 发货单据Service业务层处理
*/
@RequiredArgsConstructor
@Service
public class GearShippingOrderServiceImpl implements IGearShippingOrderService {
private final GearShippingOrderMapper baseMapper;
private final GearOrderMapper orderMapper;
@Override
public GearShippingOrderVo queryById(Long shippingId) {
return baseMapper.selectVoById(shippingId);
}
@Override
public TableDataInfo<GearShippingOrderVo> queryPageList(GearShippingOrderBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<GearShippingOrder> lqw = buildQueryWrapper(bo);
Page<GearShippingOrderVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
@Override
public List<GearShippingOrderVo> queryList(GearShippingOrderBo bo) {
LambdaQueryWrapper<GearShippingOrder> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
@Override
public List<GearShippingOrderVo> queryListByOrderId(Long orderId) {
GearShippingOrderBo bo = new GearShippingOrderBo();
bo.setOrderId(orderId);
return queryList(bo);
}
private LambdaQueryWrapper<GearShippingOrder> buildQueryWrapper(GearShippingOrderBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<GearShippingOrder> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getShippingId() != null, GearShippingOrder::getShippingId, bo.getShippingId());
lqw.like(StringUtils.isNotBlank(bo.getShippingNo()), GearShippingOrder::getShippingNo, bo.getShippingNo());
lqw.like(StringUtils.isNotBlank(bo.getShippingName()), GearShippingOrder::getShippingName, bo.getShippingName());
lqw.eq(bo.getPlanId() != null, GearShippingOrder::getPlanId, bo.getPlanId());
// receiverCustomerId 是“真实收货客户归属”,用于客户管理页按收货客户精确查询发货单据
lqw.eq(bo.getReceiverCustomerId() != null, GearShippingOrder::getReceiverCustomerId, bo.getReceiverCustomerId());
if (bo.getOrderId() != null) {
// 精确查某订单的发货单据
lqw.eq(GearShippingOrder::getOrderId, bo.getOrderId());
} else if (bo.getCustomerId() != null || bo.getSalesmanId() != null) {
// 按客户/销售员维度查发货单据先通过订单表反查订单ID集合再过滤发货单据
List<Long> orderIds = queryOrderIdsByCustomerOrSalesman(bo.getCustomerId(), bo.getSalesmanId());
if (orderIds.isEmpty()) {
lqw.eq(GearShippingOrder::getShippingId, -1L);
} else {
lqw.in(GearShippingOrder::getOrderId, orderIds);
}
}
lqw.like(StringUtils.isNotBlank(bo.getOrderCode()), GearShippingOrder::getOrderCode, bo.getOrderCode());
lqw.like(StringUtils.isNotBlank(bo.getReceiverCompany()), GearShippingOrder::getReceiverCompany, bo.getReceiverCompany());
lqw.like(StringUtils.isNotBlank(bo.getResponsibleName()), GearShippingOrder::getResponsibleName, bo.getResponsibleName());
lqw.like(StringUtils.isNotBlank(bo.getLogisticsNo()), GearShippingOrder::getLogisticsNo, bo.getLogisticsNo());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), GearShippingOrder::getStatus, bo.getStatus());
lqw.ge(bo.getShipTimeStart() != null, GearShippingOrder::getShipTime, bo.getShipTimeStart());
lqw.le(bo.getShipTimeEnd() != null, GearShippingOrder::getShipTime, bo.getShipTimeEnd());
lqw.orderByDesc(GearShippingOrder::getShipTime);
lqw.orderByDesc(GearShippingOrder::getCreateTime);
return lqw;
}
private List<Long> queryOrderIdsByCustomerOrSalesman(Long customerId, Long salesmanId) {
LambdaQueryWrapper<GearOrder> lqw = Wrappers.lambdaQuery();
lqw.select(GearOrder::getOrderId);
lqw.eq(customerId != null, GearOrder::getCustomerId, customerId);
lqw.eq(salesmanId != null, GearOrder::getSalesmanId, salesmanId);
lqw.eq(GearOrder::getDelFlag, 0);
List<GearOrder> list = orderMapper.selectList(lqw);
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
return list.stream().map(GearOrder::getOrderId).filter(id -> id != null).distinct().collect(Collectors.toList());
}
@Override
public Boolean insertByBo(GearShippingOrderBo bo) {
GearShippingOrder add = BeanUtil.toBean(bo, GearShippingOrder.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setShippingId(add.getShippingId());
syncOrderStatusIfShipped(add);
}
return flag;
}
@Override
public Boolean updateByBo(GearShippingOrderBo bo) {
GearShippingOrder update = BeanUtil.toBean(bo, GearShippingOrder.class);
validEntityBeforeSave(update);
boolean ok = baseMapper.updateById(update) > 0;
if (ok) {
syncOrderStatusIfShipped(update);
}
return ok;
}
private void syncOrderStatusIfShipped(GearShippingOrder entity) {
if (entity == null || entity.getOrderId() == null) return;
Integer statusNum = null;
try {
if (StringUtils.isNotBlank(entity.getStatus())) {
statusNum = Integer.parseInt(entity.getStatus());
}
} catch (Exception ignored) {
statusNum = null;
}
if (statusNum == null) return;
// 发货单据状态与订单状态联动:
// - 发货单据 >=2已发货若订单仍为预订单(0),推进到进行中(1)
// - 发货单据 >=3已完成若订单为预订单(0)/进行中(1),推进到已完成(2)
// - 订单若已取消(3),不做推进
if (statusNum >= 3) {
orderMapper.update(null, Wrappers.<GearOrder>lambdaUpdate()
.eq(GearOrder::getOrderId, entity.getOrderId())
.ne(GearOrder::getOrderStatus, 3)
.in(GearOrder::getOrderStatus, 0, 1)
.set(GearOrder::getOrderStatus, 2));
return;
}
if (statusNum >= 2) {
orderMapper.update(null, Wrappers.<GearOrder>lambdaUpdate()
.eq(GearOrder::getOrderId, entity.getOrderId())
.eq(GearOrder::getOrderStatus, 0)
.set(GearOrder::getOrderStatus, 1));
}
}
private void validEntityBeforeSave(GearShippingOrder entity) {
boolean isInsert = entity.getShippingId() == null;
// 发货单据ID项目内大部分业务表使用雪花ID这里保持一致
if (entity.getShippingId() == null) {
entity.setShippingId(IdUtil.getSnowflakeNextId());
}
if (isInsert) {
// 发货单号:为空则自动生成
if (StringUtils.isBlank(entity.getShippingNo())) {
entity.setShippingNo("SHIP_" + IdUtil.getSnowflakeNextIdStr());
}
// 发货单名称:为空则默认等于发货单号(便于列表展示)
if (StringUtils.isBlank(entity.getShippingName())) {
entity.setShippingName(entity.getShippingNo());
}
// 默认状态0正常
if (StringUtils.isBlank(entity.getStatus())) {
entity.setStatus("0");
}
// 默认逻辑删除标志0存在TableLogic 会在删除时写 2
if (StringUtils.isBlank(entity.getDelFlag())) {
entity.setDelFlag("0");
}
}
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid) {
// 可在此处加入业务校验:例如已对账/已开票的发货单据不允许删除
}
return baseMapper.deleteBatchIds(ids) > 0;
}
}

View File

@@ -0,0 +1,111 @@
package com.gear.oa.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.gear.common.core.domain.PageQuery;
import com.gear.common.core.page.TableDataInfo;
import com.gear.common.exception.ServiceException;
import com.gear.common.utils.StringUtils;
import com.gear.oa.domain.GearShippingOrder;
import com.gear.oa.domain.GearShippingPlan;
import com.gear.oa.domain.bo.GearShippingPlanBo;
import com.gear.oa.domain.vo.GearShippingPlanVo;
import com.gear.oa.mapper.GearShippingOrderMapper;
import com.gear.oa.mapper.GearShippingPlanMapper;
import com.gear.oa.service.IGearShippingPlanService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
/**
* 发货计划Service业务层处理
*/
@RequiredArgsConstructor
@Service
public class GearShippingPlanServiceImpl implements IGearShippingPlanService {
private final GearShippingPlanMapper baseMapper;
private final GearShippingOrderMapper shippingOrderMapper;
@Override
public GearShippingPlanVo queryById(Long planId) {
return baseMapper.selectVoById(planId);
}
@Override
public TableDataInfo<GearShippingPlanVo> queryPageList(GearShippingPlanBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<GearShippingPlan> lqw = buildQueryWrapper(bo);
Page<GearShippingPlanVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
@Override
public List<GearShippingPlanVo> queryList(GearShippingPlanBo bo) {
LambdaQueryWrapper<GearShippingPlan> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
@Override
public List<GearShippingPlanVo> queryListWithCount(GearShippingPlanBo bo) {
return baseMapper.selectVoListWithCount(bo);
}
private LambdaQueryWrapper<GearShippingPlan> buildQueryWrapper(GearShippingPlanBo bo) {
LambdaQueryWrapper<GearShippingPlan> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getPlanId() != null, GearShippingPlan::getPlanId, bo.getPlanId());
lqw.like(StringUtils.isNotBlank(bo.getPlanName()), GearShippingPlan::getPlanName, bo.getPlanName());
lqw.eq(bo.getPlanDate() != null, GearShippingPlan::getPlanDate, bo.getPlanDate());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), GearShippingPlan::getStatus, bo.getStatus());
lqw.orderByDesc(GearShippingPlan::getPlanDate);
lqw.orderByDesc(GearShippingPlan::getCreateTime);
return lqw;
}
@Override
public Boolean insertByBo(GearShippingPlanBo bo) {
GearShippingPlan add = BeanUtil.toBean(bo, GearShippingPlan.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setPlanId(add.getPlanId());
}
return flag;
}
@Override
public Boolean updateByBo(GearShippingPlanBo bo) {
GearShippingPlan update = BeanUtil.toBean(bo, GearShippingPlan.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
private void validEntityBeforeSave(GearShippingPlan entity) {
if (entity.getPlanId() == null) {
entity.setPlanId(IdUtil.getSnowflakeNextId());
}
if (StringUtils.isBlank(entity.getStatus())) {
entity.setStatus("0");
}
if (StringUtils.isBlank(entity.getDelFlag())) {
entity.setDelFlag("0");
}
}
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if (isValid && ids != null && !ids.isEmpty()) {
Long cnt = shippingOrderMapper.selectCount(Wrappers.lambdaQuery(GearShippingOrder.class)
.in(GearShippingOrder::getPlanId, ids));
if (cnt != null && cnt > 0) {
throw new ServiceException("该计划下存在发货单据,无法删除");
}
}
return baseMapper.deleteBatchIds(ids) > 0;
}
}

View File

@@ -25,6 +25,7 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.HashMap;
import java.util.Map;
/**
@@ -176,6 +177,49 @@ public class GearStockServiceImpl implements IGearStockService {
return baseMapper.deleteBatchIds(ids) > 0;
}
@Override
public Map<Long, BigDecimal> sumQuantityByItemIds(String itemType, List<Long> itemIds) {
if (StringUtils.isBlank(itemType) || itemIds == null || itemIds.isEmpty()) {
return new HashMap<>();
}
List<Map<String, Object>> rows = baseMapper.selectSumQuantityByItemIds(itemType, itemIds);
Map<Long, BigDecimal> result = new HashMap<>();
if (rows == null || rows.isEmpty()) {
return result;
}
for (Map<String, Object> r : rows) {
if (r == null) continue;
Object idObj = r.get("itemId");
Object qtyObj = r.get("quantity");
if (idObj == null) continue;
Long id = null;
if (idObj instanceof Number) {
id = ((Number) idObj).longValue();
} else {
try {
id = Long.valueOf(String.valueOf(idObj));
} catch (Exception ignored) {
id = null;
}
}
if (id == null) continue;
BigDecimal qty = BigDecimal.ZERO;
if (qtyObj instanceof BigDecimal) {
qty = (BigDecimal) qtyObj;
} else if (qtyObj instanceof Number) {
qty = BigDecimal.valueOf(((Number) qtyObj).doubleValue());
} else if (qtyObj != null) {
try {
qty = new BigDecimal(String.valueOf(qtyObj));
} catch (Exception ignored) {
qty = BigDecimal.ZERO;
}
}
result.put(id, qty);
}
return result;
}
// @Override
// public BigDecimal getStockByItemId(Long rawMaterialId) {
// return baseMapper.getStockByItemId(rawMaterialId);

View File

@@ -20,15 +20,31 @@
<result property="noTaxPrice" column="no_tax_price"/>
</resultMap>
<select id="selectVoListByOrderId" resultType="com.gear.oa.domain.vo.GearOrderDetailVo">
SELECT * FROM gear_order_detail WHERE order_id = #{orderId} AND del_flag = 0
SELECT
d.*,
COALESCE(p.product_name, mp.product_name) AS productName,
COALESCE(p.product_code, CAST(mp.product_id AS CHAR)) AS productCode,
COALESCE(p.type, mp.product_type) AS productType,
mp.spec AS spec,
mp.model AS model,
mp.unit_price AS unitPrice
FROM gear_order_detail d
LEFT JOIN gear_product p ON d.product_id = p.product_id
LEFT JOIN mat_product mp ON d.product_id = mp.product_id
WHERE d.order_id = #{orderId} AND d.del_flag = 0
</select>
<select id="selectVoPagePlus" resultType="com.gear.oa.domain.vo.GearOrderDetailVo">
SELECT
d.*,
p.product_name AS productName,
p.product_code AS productCode
COALESCE(p.product_name, mp.product_name) AS productName,
COALESCE(p.product_code, CAST(mp.product_id AS CHAR)) AS productCode,
COALESCE(p.type, mp.product_type) AS productType,
mp.spec AS spec,
mp.model AS model,
mp.unit_price AS unitPrice
FROM gear_order_detail d
LEFT JOIN gear_product p ON d.product_id = p.product_id
LEFT JOIN mat_product mp ON d.product_id = mp.product_id
${ew.customSqlSegment}
</select>

View File

@@ -10,6 +10,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="customerId" column="customer_id"/>
<result property="company" column="company"/>
<result property="salesManager" column="sales_manager"/>
<result property="salesmanId" column="salesman_id"/>
<result property="orderStatus" column="order_status"/>
<result property="tradeType" column="trade_type"/>
<result property="remark" column="remark"/>
@@ -20,14 +21,29 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<result property="updateBy" column="update_by"/>
<result property="taxAmount" column="tax_amount"/>
<result property="noTaxAmount" column="no_tax_amount"/>
<result property="contractExcelOssIds" column="contract_excel_oss_ids"/>
</resultMap>
<select id="selectVoPlusPage" resultType="com.gear.oa.domain.vo.GearOrderVo">
select o.order_id,
o.order_code,
o.customer_id,
o.company,
o.sales_manager,
o.order_status,
o.salesman_id,
s.name as sales_manager,
(CASE
WHEN o.order_status = 3 THEN 3
WHEN EXISTS (SELECT 1
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 3) THEN 2
WHEN EXISTS (SELECT 1
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 2) THEN 1
ELSE o.order_status
END) AS order_status,
o.remark,
o.del_flag,
o.create_time,
@@ -36,9 +52,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
o.update_by,
o.tax_amount,
o.no_tax_amount,
c.name as customerName
o.contract_excel_oss_ids as contractExcelOssIds,
c.name as customerName,
(SELECT MAX(CAST(so.status AS SIGNED))
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0') AS shippingMaxStatus,
(CASE
WHEN EXISTS (SELECT 1
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 2) THEN 1
ELSE 0
END) AS shippedFlag,
(SELECT MAX(so.ship_time)
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 2) AS lastShipTime
from gear_order o
left join gear_customer c on o.customer_id = c.customer_id and c.del_flag = 0
left join gear_salesman s on o.salesman_id = s.salesman_id and s.del_flag = 0
${ew.customSqlSegment}
</select>
<select id="selectVoPagePlus" resultType="com.gear.oa.domain.vo.GearOrderVo">
@@ -46,8 +81,22 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
o.order_code,
o.customer_id,
o.company,
o.sales_manager,
o.order_status,
o.salesman_id,
s.name as sales_manager,
(CASE
WHEN o.order_status = 3 THEN 3
WHEN EXISTS (SELECT 1
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 3) THEN 2
WHEN EXISTS (SELECT 1
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 2) THEN 1
ELSE o.order_status
END) AS order_status,
o.remark,
o.del_flag,
o.create_time,
@@ -56,9 +105,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
o.update_by,
o.tax_amount,
o.no_tax_amount,
c.name as customerName
o.contract_excel_oss_ids as contractExcelOssIds,
c.name as customerName,
(SELECT MAX(CAST(so.status AS SIGNED))
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0') AS shippingMaxStatus,
(CASE
WHEN EXISTS (SELECT 1
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 2) THEN 1
ELSE 0
END) AS shippedFlag,
(SELECT MAX(so.ship_time)
FROM gear_shipping_order so
WHERE so.order_id = o.order_id
AND so.del_flag = '0'
AND CAST(so.status AS SIGNED) >= 2) AS lastShipTime
from gear_order o
left join gear_customer c on o.customer_id = c.customer_id and c.del_flag = 0
left join gear_salesman s on o.salesman_id = s.salesman_id and s.del_flag = 0
${ew.customSqlSegment}
</select>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.gear.oa.mapper.GearOrderProductionMapper">
<resultMap type="com.gear.oa.domain.GearOrderProduction" id="GearOrderProductionResult">
<result property="productionId" column="production_id"/>
<result property="orderId" column="order_id"/>
<result property="orderDetailId" column="order_detail_id"/>
<result property="productId" column="product_id"/>
<result property="planQty" column="plan_qty"/>
<result property="finishedQty" column="finished_qty"/>
<result property="badQty" column="bad_qty"/>
<result property="unit" column="unit"/>
<result property="remark" column="remark"/>
<result property="delFlag" column="del_flag"/>
<result property="createTime" column="create_time"/>
<result property="createBy" column="create_by"/>
<result property="updateTime" column="update_time"/>
<result property="updateBy" column="update_by"/>
</resultMap>
<select id="selectVoPagePlus" resultType="com.gear.oa.domain.vo.GearOrderProductionVo">
SELECT
p.*,
COALESCE(gp.product_name, mp.product_name) AS productName,
COALESCE(gp.product_code, CAST(mp.product_id AS CHAR)) AS productCode,
COALESCE(gp.type, mp.product_type) AS productType,
mp.spec AS spec,
mp.model AS model
FROM gear_order_production p
LEFT JOIN gear_order_detail d ON p.order_detail_id = d.detail_id
LEFT JOIN gear_product gp ON d.product_id = gp.product_id
LEFT JOIN mat_product mp ON d.product_id = mp.product_id
${ew.customSqlSegment}
</select>
<select id="selectVoListPlus" resultType="com.gear.oa.domain.vo.GearOrderProductionVo">
SELECT
p.*,
COALESCE(gp.product_name, mp.product_name) AS productName,
COALESCE(gp.product_code, CAST(mp.product_id AS CHAR)) AS productCode,
COALESCE(gp.type, mp.product_type) AS productType,
mp.spec AS spec,
mp.model AS model
FROM gear_order_production p
LEFT JOIN gear_order_detail d ON p.order_detail_id = d.detail_id
LEFT JOIN gear_product gp ON d.product_id = gp.product_id
LEFT JOIN mat_product mp ON d.product_id = mp.product_id
${ew.customSqlSegment}
</select>
</mapper>

View File

@@ -25,6 +25,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
where item_id = #{rawMaterialId} and item_type = 'raw_material' and del_flag = 0
</select>
<select id="selectSumQuantityByItemIds" resultType="java.util.HashMap">
SELECT item_id AS itemId, SUM(quantity) AS quantity
FROM gear_stock
WHERE del_flag = 0
AND item_type = #{itemType}
AND item_id IN
<foreach collection="itemIds" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
GROUP BY item_id
</select>
<!-- 分页联查物品名称和编码支持Wrapper动态条件返回Page<GearStockVo> -->
<select id="selectVoPagePlus" resultType="com.gear.oa.domain.vo.GearStockVo">
SELECT

View File

@@ -18,6 +18,8 @@
"dependencies": {
"@element-plus/icons-vue": "2.3.1",
"@tailwindcss/vite": "^4.1.11",
"@vue-office/docx": "^1.6.3",
"@vue-office/excel": "^1.7.14",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "13.3.0",
"axios": "1.9.0",
@@ -27,6 +29,7 @@
"element-plus": "2.9.9",
"file-saver": "2.0.5",
"fuse.js": "6.6.2",
"html2canvas": "^1.4.1",
"i": "^0.3.7",
"js-beautify": "1.14.11",
"js-cookie": "3.0.5",

View File

@@ -69,3 +69,15 @@ export function importProductData(data, updateSupport) {
data: data
})
}
// 查询产品附加属性按产品ID集合返回 Map<productId, attrs>
export function listProductAdditionByProductIds(productIds) {
return request({
url: '/api/mat/productAddition/listByProductIds',
method: 'post',
data: productIds,
headers: {
repeatSubmit: false
}
})
}

View File

@@ -47,6 +47,9 @@ export function listProductAdditionByProductIds(productIds) {
return request({
url: '/api/mat/productAddition/listByProductIds',
method: 'post',
data: productIds
data: productIds,
headers: {
repeatSubmit: false
}
})
}

View File

@@ -0,0 +1,27 @@
import request from '@/utils/request'
// 订单生产记录gear_order_production
export function listOrderProduction(query) {
return request({
url: '/oa/orderProduction/list',
method: 'get',
params: query
})
}
export function initOrderProduction(orderId) {
return request({
url: '/oa/orderProduction/init/' + orderId,
method: 'post'
})
}
export function updateOrderProduction(data) {
return request({
url: '/oa/orderProduction',
method: 'put',
data
})
}

View File

@@ -0,0 +1,54 @@
import request from '@/utils/request'
// 查询销售员列表
export function listSalesman(query) {
return request({
url: '/oa/salesman/list',
method: 'get',
params: query
})
}
// 查询销售员详情
export function getSalesman(salesmanId) {
return request({
url: '/oa/salesman/' + salesmanId,
method: 'get'
})
}
// 新增销售员
export function addSalesman(data) {
return request({
url: '/oa/salesman',
method: 'post',
data
})
}
// 修改销售员
export function updateSalesman(data) {
return request({
url: '/oa/salesman',
method: 'put',
data
})
}
// 删除销售员
export function delSalesman(salesmanIds) {
return request({
url: '/oa/salesman/' + salesmanIds,
method: 'delete'
})
}
// 查询销售员跟进客户
export function listSalesmanCustomers(salesmanId, query) {
return request({
url: '/oa/salesman/' + salesmanId + '/customers',
method: 'get',
params: query
})
}

View File

@@ -0,0 +1,111 @@
import request from '@/utils/request'
// 发货单据(独立表 gear_shipping_order
// 说明:用于订单发货/物流信息记录,不与出入库/WMS 单据绑定
// 查询发货单据列表
export function listShippingOrder(query) {
return request({
url: '/oa/shippingOrder/list',
method: 'get',
params: query
})
}
// 根据订单ID查询发货单据列表
export function listShippingOrderByOrderId(orderId) {
return request({
url: '/oa/shippingOrder/listByOrderId/' + orderId,
method: 'get'
})
}
// 查询发货单据详细
export function getShippingOrder(shippingId) {
return request({
url: '/oa/shippingOrder/' + shippingId,
method: 'get'
})
}
// 新增发货单据
export function addShippingOrder(data) {
return request({
url: '/oa/shippingOrder',
method: 'post',
data: data
})
}
// 修改发货单据
export function updateShippingOrder(data) {
return request({
url: '/oa/shippingOrder',
method: 'put',
data: data
})
}
// 删除发货单据
export function delShippingOrder(shippingId) {
return request({
url: '/oa/shippingOrder/' + shippingId,
method: 'delete'
})
}
// ================================
// 发货计划(独立表 gear_shipping_plan
// ================================
// 查询发货计划列表(分页)
export function listShippingPlan(query) {
return request({
url: '/oa/shippingPlan/list',
method: 'get',
params: query
})
}
// 查询发货计划列表(带单据数,左侧列表使用)
export function listShippingPlanWithCount(query) {
return request({
url: '/oa/shippingPlan/listWithCount',
method: 'get',
params: query
})
}
// 查询发货计划详细
export function getShippingPlan(planId) {
return request({
url: '/oa/shippingPlan/' + planId,
method: 'get'
})
}
// 新增发货计划
export function addShippingPlan(data) {
return request({
url: '/oa/shippingPlan',
method: 'post',
data: data
})
}
// 修改发货计划
export function updateShippingPlan(data) {
return request({
url: '/oa/shippingPlan',
method: 'put',
data: data
})
}
// 删除发货计划
export function delShippingPlan(planId) {
return request({
url: '/oa/shippingPlan/' + planId,
method: 'delete'
})
}

View File

@@ -53,3 +53,17 @@ export function getStockTrace(batchNo) {
}
})
}
export function sumStockQuantityByItemIds(itemType, itemIds) {
return request({
url: '/gear/stock/sumQuantityByItemIds',
method: 'post',
params: {
itemType: itemType || 'product'
},
data: itemIds || [],
headers: {
repeatSubmit: false
}
})
}

View File

@@ -88,13 +88,13 @@ const renderChart = () => {
if (!productStats[productName]) {
productStats[productName] = {
quantity: 0, // 销量
amount: 0 // 销售额(销量*含税单价)
amount: 0 // 销售额(销量*单价)
};
}
// 累加销量
productStats[productName].quantity += Number(detail.quantity || 0);
// 累加销售额
const unitPrice = Number(detail.taxPrice || 0);
const unitPrice = Number(detail.unitPrice || 0);
productStats[productName].amount += unitPrice * Number(detail.quantity || 0);
});
@@ -200,4 +200,4 @@ const handleResize = () => {
width: 100%;
height: calc(100% - 40px);
}
</style>
</style>

View File

@@ -0,0 +1,626 @@
<template>
<div class="app-container sales-order-page">
<!-- 左右两栏左侧合同列表 + 右侧详情 -->
<el-row :gutter="16">
<!-- 左侧列表区 -->
<el-col :span="6" class="left-pane">
<!-- 顶部搜索 + 操作 -->
<div class="left-toolbar">
<el-input
v-model="keyword"
clearable
placeholder="请输入关键字"
@clear="handleQuery"
@keyup.enter="handleQuery"
>
<template #append>
<el-button icon="Search" @click="handleQuery" />
</template>
</el-input>
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
</div>
<!-- 列表使用现有 GearList 组件实现卡片式 -->
<klp-list
:list-data="filteredList"
list-key="contractId"
:loading="loading"
info1-field="contractTitle"
info1-max-percent="70"
info5-field="contractCode"
@item-click="handleSelect"
>
<!-- 主标题合同名称 + 合同编号 -->
<template #info1="{ item }">
<span class="list-title">{{ item.contractTitle }}</span>
<span class="list-code">{{ item.contractCode }}</span>
</template>
<!-- 副标题/乙方 -->
<template #info2="{ item }">
<div class="list-sub">
<span>供方{{ item.sellerName }}</span>
<span class="ml-8">需方{{ item.buyerName }}</span>
</div>
</template>
<!-- 状态 -->
<template #info3="{ item }">
<el-tag :type="item.status === '已生效' ? 'success' : 'info'" size="small">
{{ item.status }}
</el-tag>
</template>
<!-- 时间信息 -->
<template #info4="{ item }">
<div class="list-sub">
<span>签订时间{{ item.signDate }}</span>
<span class="ml-8">交货日期{{ item.deliveryDate }}</span>
</div>
</template>
<!-- 右侧动作置顶/导出/修改/删除雏形只做 UI -->
<template #actions="{ item }">
<div class="list-actions">
<el-button link type="primary" icon="Top" @click.stop="handlePin(item)">置顶</el-button>
<el-button link type="primary" icon="Download" @click.stop="handleExport(item)">导出</el-button>
<el-button link type="primary" icon="Edit" @click.stop="handleEdit(item)">修改</el-button>
<el-button link type="danger" icon="Delete" @click.stop="handleDelete(item)">删除</el-button>
</div>
</template>
</klp-list>
</el-col>
<!-- 右侧详情区 -->
<el-col :span="18" class="right-pane">
<!-- 未选中时提示 -->
<div v-if="!selected" class="empty-tip">
<el-empty description="请选择左侧一个订单查看详情" />
</div>
<!-- 选中后显示详情 -->
<div v-else>
<!-- 顶部标题 + 状态选择雏形 -->
<div class="detail-header">
<div class="detail-header__title">
<span class="detail-title">产品销售合同</span>
<span class="detail-code">{{ selected.contractCode }}</span>
</div>
<el-select v-model="selected.contractStatus" size="small" style="width: 140px">
<el-option label="草稿" value="草稿" />
<el-option label="已生效" value="已生效" />
<el-option label="已作废" value="已作废" />
</el-select>
</div>
<!-- 合同基础信息预览Excel表风格展示对齐产品管理-说明书预览的观感边框+圆角+表格单元格 -->
<div class="contract-excel-preview">
<div class="contract-excel-title">
<span>合同基础信息</span>
<div class="contract-excel-title__actions">
<!-- 上传合同Excel雏形直接保存ossId串到 selected.contractExcelOssIds后续可落库到订单主表 -->
<file-upload
v-model="selected.contractExcelOssIds"
:limit="1"
:file-size="20"
:file-type="['xls', 'xlsx']"
:is-show-tip="false"
@success="handleContractExcelUploaded"
/>
</div>
</div>
<div class="contract-excel-body">
<table class="excel-table">
<colgroup>
<col style="width: 14%" />
<col style="width: 36%" />
<col style="width: 14%" />
<col style="width: 36%" />
</colgroup>
<tbody>
<tr>
<td class="excel-cell excel-cell--label">合同编号</td>
<td class="excel-cell">{{ selected.contractCode }}</td>
<td class="excel-cell excel-cell--label">合同状态</td>
<td class="excel-cell">{{ selected.contractStatus }}</td>
</tr>
<tr>
<td class="excel-cell excel-cell--label">供方</td>
<td class="excel-cell">{{ selected.sellerName }}</td>
<td class="excel-cell excel-cell--label">需方</td>
<td class="excel-cell">{{ selected.buyerName }}</td>
</tr>
<tr>
<td class="excel-cell excel-cell--label">签订时间</td>
<td class="excel-cell">{{ selected.signDate }}</td>
<td class="excel-cell excel-cell--label">交货日期</td>
<td class="excel-cell">{{ selected.deliveryDate }}</td>
</tr>
<tr>
<td class="excel-cell excel-cell--label">签订地点</td>
<td class="excel-cell">{{ selected.signPlace }}</td>
<td class="excel-cell excel-cell--label">备注</td>
<td class="excel-cell">{{ selected.remark || '—' }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 产品内容产品名称 + 生产厂家 -->
<div class="section">
<div class="section-title">产品内容</div>
<div class="section-body">
<div class="product-top">
<div class="product-top__left">
<span class="label">产品名称</span>
<el-select v-model="selected.productName" filterable clearable style="width: 240px">
<el-option v-for="name in productNameOptions" :key="name" :label="name" :value="name" />
</el-select>
</div>
<div class="product-top__right">
<span class="label">生产厂家</span>
<span class="value">{{ selected.factoryName }}</span>
</div>
</div>
<!-- 产品明细雏形表格支持行内编辑与汇总 -->
<el-table
:data="selected.items"
border
size="small"
class="detail-table"
show-summary
:summary-method="summaryMethod"
>
<el-table-column type="index" width="60" label="序号" align="center" />
<el-table-column label="规格mm" min-width="160">
<template #default="{ row }">
<el-input v-model="row.spec" size="small" />
</template>
</el-table-column>
<el-table-column label="材质" width="120">
<template #default="{ row }">
<el-input v-model="row.material" size="small" />
</template>
</el-table-column>
<el-table-column label="数量(吨)" width="140" align="right">
<template #default="{ row }">
<el-input-number v-model="row.qty" :controls="false" :min="0" size="small" style="width: 120px" />
</template>
</el-table-column>
<el-table-column label="备注" min-width="160">
<template #default="{ row }">
<el-input v-model="row.remark" size="small" />
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 业务 Tab雏形占位 -->
<el-tabs v-model="activeTab" type="border-card" class="mt-12">
<el-tab-pane label="订单信息" name="orderInfo">
<el-empty description="订单信息(待完善)" />
</el-tab-pane>
<el-tab-pane label="财务状态" name="finance">
<div class="section">
<div class="section-title">财务状态</div>
<div class="finance-strip">
<div class="finance-item">
<div class="finance-item__label">订单金额</div>
<div class="finance-item__value">{{ formatMoney(financeSummary.orderAmount) }}</div>
</div>
<div class="finance-item">
<div class="finance-item__label">已收款金额</div>
<div class="finance-item__value">{{ formatMoney(financeSummary.receivedAmount) }}</div>
</div>
<div class="finance-item">
<div class="finance-item__label">未收款金额</div>
<div class="finance-item__value">{{ formatMoney(financeSummary.unreceivedAmount) }}</div>
</div>
</div>
<div class="section-title mt-12">收款明细</div>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" size="small" @click="handleAddPayment">新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" size="small" :disabled="true">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" size="small" :disabled="true">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" size="small" @click="handleExportPayments">导出</el-button>
</el-col>
</el-row>
<el-table :data="paymentList" border size="small">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="收款日期" prop="payDate" width="160" />
<el-table-column label="收款金额" prop="amount" width="160" align="right">
<template #default="{ row }">
{{ formatMoney(row.amount) }}
</template>
</el-table-column>
<el-table-column label="备注" prop="remark" />
<el-table-column label="操作" width="140" align="center">
<template #default>
<el-button link type="primary" size="small" icon="Edit">修改</el-button>
<el-button link type="danger" size="small" icon="Delete">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-tab-pane>
<el-tab-pane label="生产成本" name="cost">
<el-empty description="生产成本(待完善)" />
</el-tab-pane>
<el-tab-pane label="发货流程" name="ship">
<el-empty description="发货流程(待完善)" />
</el-tab-pane>
<el-tab-pane label="合同附件" name="files">
<el-empty description="合同附件(待完善)" />
</el-tab-pane>
<el-tab-pane label="操作记录" name="logs">
<el-empty description="操作记录(待完善)" />
</el-tab-pane>
</el-tabs>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import klpList from "@/components/GearList/index.vue";
import FileUpload from "@/components/FileUpload/index.vue";
export default {
name: "MatSalesOrder",
components: { klpList, FileUpload },
data() {
return {
// 页面加载态:雏形直接用本地数据,保留 loading 方便后续接接口
loading: false,
// 左侧搜索关键字
keyword: "",
// 当前选中的合同/订单
selected: null,
// 右侧 Tab 当前激活项
activeTab: "finance",
// 产品名称下拉:雏形静态配置,后续可接产品字典/接口
productNameOptions: ["冷轧钢卷", "热轧钢卷", "镀锌卷", "不锈钢卷"],
// 左侧列表:雏形静态数据,后续替换为接口分页
list: [
{
contractId: 1,
contractTitle: "产品销售合同",
contractCode: "KLPYX260420-14",
sellerName: "嘉祥科伦管理有限公司",
buyerName: "洛阳一石科技有限公司",
signDate: "2026-05-12",
deliveryDate: "2026-06-15",
signPlace: "嘉祥",
status: "草稿",
contractStatus: "草稿",
productName: "冷轧钢卷",
factoryName: "嘉祥科伦管理有限公司",
remark: "",
// 合同Excel附件ossId串逗号分隔
contractExcelOssIds: "",
items: [
{ spec: "0.38*1200", material: "SPCC", qty: 10, remark: "净边料 角包 卷" }
]
},
{
contractId: 2,
contractTitle: "产品销售合同",
contractCode: "KLPYX260420-06",
sellerName: "嘉祥科伦管理有限公司",
buyerName: "湖北广化工贸有限公司",
signDate: "2026-04-20",
deliveryDate: "2026-05-25",
signPlace: "嘉祥",
status: "已生效",
contractStatus: "已生效",
productName: "热轧钢卷",
factoryName: "嘉祥科伦管理有限公司",
remark: "",
// 合同Excel附件ossId串逗号分隔
contractExcelOssIds: "",
items: [
{ spec: "2.0*1250", material: "Q235B", qty: 5, remark: "" }
]
}
],
// 财务状态:雏形数据
financeSummary: {
orderAmount: 30000,
receivedAmount: 0,
unreceivedAmount: 30000
},
// 收款明细:雏形为空数据
paymentList: []
};
},
computed: {
// 左侧列表过滤:关键字匹配合同号/甲乙方
filteredList() {
const kw = (this.keyword || "").trim();
if (!kw) return this.list;
return this.list.filter((it) => {
return (
(it.contractCode || "").includes(kw) ||
(it.sellerName || "").includes(kw) ||
(it.buyerName || "").includes(kw)
);
});
}
},
methods: {
// 搜索:雏形仅触发计算属性刷新,后续可接接口
handleQuery() {},
// 左侧选中:加载详情(雏形直接赋值)
handleSelect(item) {
this.selected = item || null;
this.activeTab = "finance";
this.syncFinanceSummary();
},
// 新增:雏形占位,后续可弹窗录入合同/订单信息
handleAdd() {
this.$modal && this.$modal.msgInfo && this.$modal.msgInfo("新增(雏形)");
},
// 置顶:雏形占位
handlePin() {},
// 导出:雏形占位
handleExport() {},
// 修改:雏形占位
handleEdit() {},
// 删除:雏形占位
handleDelete() {},
// 财务:新增收款明细(雏形占位)
handleAddPayment() {
this.$modal && this.$modal.msgInfo && this.$modal.msgInfo("新增收款明细(雏形)");
},
// 财务:导出收款明细(雏形占位)
handleExportPayments() {},
// 金额格式化:雏形使用 toFixed
formatMoney(val) {
const n = Number(val || 0);
if (Number.isNaN(n)) return "0.00";
return n.toFixed(2);
},
// 表格汇总:合计数量/金额(雏形)
summaryMethod({ columns, data }) {
const sums = [];
const totalQty = data.reduce((acc, it) => acc + Number(it.qty || 0), 0);
columns.forEach((col, index) => {
if (index === 0) {
sums[index] = "合计";
return;
}
if (col.label === "数量(吨)") {
sums[index] = totalQty;
return;
}
sums[index] = "";
});
return sums;
},
// 根据明细同步财务汇总(雏形:用含税总额当订单金额)
syncFinanceSummary() {
if (!this.selected) return;
const orderAmount = 0;
const receivedAmount = (this.paymentList || []).reduce((acc, it) => acc + Number(it.amount || 0), 0);
this.financeSummary.orderAmount = orderAmount;
this.financeSummary.receivedAmount = receivedAmount;
this.financeSummary.unreceivedAmount = orderAmount - receivedAmount;
},
// 合同Excel上传完成雏形仅做提示后续可在这里调用接口保存到数据库
handleContractExcelUploaded() {
this.$modal && this.$modal.msgSuccess && this.$modal.msgSuccess("合同Excel已上传");
}
},
mounted() {
// 默认选中第一条,方便看到右侧雏形效果
if (this.list.length > 0) {
this.handleSelect(this.list[0]);
}
}
};
</script>
<style scoped>
.sales-order-page {
height: calc(100vh - 120px);
}
.left-pane {
height: 100%;
}
.left-toolbar {
display: flex;
gap: 8px;
align-items: center;
margin-bottom: 10px;
}
.list-title {
font-weight: 600;
margin-right: 8px;
}
.list-code {
color: #909399;
}
.list-sub {
color: #606266;
font-size: 12px;
display: inline-flex;
flex-wrap: wrap;
}
.ml-8 {
margin-left: 8px;
}
.list-actions {
display: flex;
flex-direction: column;
gap: 2px;
align-items: flex-end;
}
.right-pane {
height: 100%;
}
.empty-tip {
height: 520px;
display: flex;
align-items: center;
justify-content: center;
}
.detail-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.detail-header__title {
display: inline-flex;
align-items: baseline;
gap: 10px;
}
.detail-title {
font-size: 16px;
font-weight: 700;
}
.detail-code {
color: #909399;
}
.contract-excel-preview {
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
overflow: hidden;
margin-bottom: 12px;
}
.contract-excel-title {
padding: 8px 10px;
font-size: 13px;
font-weight: 600;
background: var(--el-fill-color-lighter);
border-bottom: 1px solid var(--el-border-color-light);
display: flex;
align-items: center;
justify-content: space-between;
}
.contract-excel-title__actions :deep(.el-button) {
height: 28px;
padding: 0 10px;
}
.contract-excel-title__actions :deep(.upload-file-list) {
display: none;
}
.contract-excel-body {
background: #fff;
}
.excel-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.excel-cell {
border: 1px solid var(--el-border-color-light);
padding: 8px 10px;
font-size: 12px;
color: #303133;
word-break: break-word;
background: #fff;
}
.excel-cell--label {
background: var(--el-fill-color-light);
color: #606266;
font-weight: 600;
}
.section {
margin-top: 12px;
}
.section-title {
font-weight: 600;
margin-bottom: 8px;
}
.section-body {
background: #fff;
}
.product-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.label {
color: #606266;
}
.value {
font-weight: 600;
}
.detail-table {
width: 100%;
}
.mt-12 {
margin-top: 12px;
}
.finance-strip {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 12px;
}
.finance-item {
border: 1px solid #ebeef5;
border-radius: 6px;
padding: 10px 12px;
background: #fafafa;
}
.finance-item__label {
color: #909399;
font-size: 12px;
margin-bottom: 6px;
}
.finance-item__value {
font-size: 16px;
font-weight: 700;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -26,17 +26,6 @@
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="flex justify-between items-center">
<div>
<p class="text-gray-500 text-sm">总销售额</p>
<h3 class="text-2xl font-bold">¥{{ totalSales.toFixed(2) }}</h3>
</div>
<el-icon class="text-warning text-xl"><Money /></el-icon>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="flex justify-between items-center">
@@ -74,7 +63,6 @@
<span>产品销售排行</span>
<el-select v-model="productRankType" style="width: 100px;" placeholder="统计类型" size="small" @change="renderProductRankChart">
<el-option label="销量" value="quantity" />
<el-option label="销售额" value="amount" />
</el-select>
</div>
</template>
@@ -119,12 +107,6 @@
<el-table-column prop="orderCode" label="订单编号" />
<el-table-column prop="customerName" label="客户名称" />
<el-table-column prop="salesManager" label="销售经理" />
<el-table-column
prop="taxAmount"
label="订单金额"
align="right"
:formatter="(row) => `¥${Number(row.taxAmount).toFixed(2)}`"
/>
<el-table-column
prop="orderStatus"
label="订单状态"
@@ -155,7 +137,7 @@
import { ref, onMounted, computed } from 'vue'
import * as echarts from 'echarts'
import {
UserFilled, ShoppingCart, Money, RefreshRight
UserFilled, ShoppingCart, RefreshRight
} from '@element-plus/icons-vue'
import { listOrder } from '@/api/oms/order'
import { listOrderDetail } from '@/api/oms/orderDetail'
@@ -194,12 +176,6 @@ const followUpStatusLabel = {
// 指标计算computed确保响应式
const totalCustomers = computed(() => customers.value.length)
const totalOrders = computed(() => orders.value.length)
const totalSales = computed(() => {
return orders.value.reduce(
(sum, order) => sum + Number(order.taxAmount || 0),
0
)
})
const returnExchangeRate = computed(() => {
if (totalOrders.value === 0) return 0
return (returnExchanges.value.length / totalOrders.value) * 100
@@ -250,7 +226,7 @@ onMounted(async () => {
// ---------- 图表渲染函数 ----------
// 订单趋势图(双轴:订单数+销售额
// 订单趋势图(订单数
const renderOrderTrendChart = () => {
const chartDom = orderTrendChart.value
if (!chartDom) return
@@ -266,19 +242,16 @@ const renderOrderTrendChart = () => {
dates.push(date.toLocaleDateString())
}
// 按日期统计订单数和销售额
// 按日期统计订单数
const orderCountMap = {}
const salesMap = {}
dates.forEach(date => {
orderCountMap[date] = 0
salesMap[date] = 0
})
orders.value.forEach(order => {
// 假设订单有createTime字段需与dates格式匹配
const orderDate = new Date(order.createTime).toLocaleDateString()
if (orderCountMap[orderDate] !== undefined) {
orderCountMap[orderDate]++
salesMap[orderDate] += Number(order.taxAmount || 0)
}
})
@@ -287,35 +260,19 @@ const renderOrderTrendChart = () => {
trigger: 'axis',
axisPointer: { type: 'cross' }
},
legend: { data: ['订单数', '销售额'] },
legend: { data: ['订单数'] },
xAxis: {
type: 'category',
data: dates,
axisLabel: { rotate: 30, fontSize: 12 }
},
yAxis: [
{ type: 'value', name: '订单数', min: 0 },
{
type: 'value',
name: '销售额',
min: 0,
axisLabel: { formatter: '¥{value}' }
}
],
yAxis: { type: 'value', name: '订单数', min: 0 },
series: [
{
name: '订单数',
type: 'bar',
data: dates.map(date => orderCountMap[date]),
barWidth: '40%'
},
{
name: '销售额',
type: 'line',
yAxisIndex: 1,
data: dates.map(date => salesMap[date]),
smooth: true,
lineStyle: { width: 2 }
}
]
}
@@ -340,7 +297,7 @@ const renderProductRankChart = () => {
}
productStats[productName].quantity += Number(detail.quantity || 0)
productStats[productName].amount +=
Number(detail.quantity || 0) * Number(detail.taxPrice || 0)
Number(detail.quantity || 0) * Number(detail.unitPrice || 0)
})
// 转换为图表数据取Top10
@@ -478,4 +435,4 @@ const viewOrderDetail = (row) => {
width: 100%;
height: 300px;
}
</style>
</style>

View File

@@ -16,26 +16,6 @@
<el-col :span="1.5">
<el-button type="warning" plain icon="Download" size="small" @click="handleExport">导出</el-button>
</el-col>
<!-- 含税部分 -->
<el-col :span="8">
<span>含税金额</span>
<span style="margin-right: 10px;">{{ orderInfo?.taxAmount }}</span>
<span style="margin-right: 10px;">-</span>
<span style="margin-right: 10px;">{{ actualAmount }}</span>
<span style="margin-right: 10px;">=</span>
<span style="color: red; margin-right: 10px;">{{ amountDifference }}</span>
</el-col>
<!-- 无税部分 -->
<el-col :span="8">
<span>无税金额</span>
<span style="margin-right: 10px;">{{ orderInfo?.noTaxAmount }}</span>
<span style="margin-right: 10px;">-</span>
<span style="margin-right: 10px;">{{ noTaxAmount }}</span>
<span style="margin-right: 10px;">=</span>
<span style="color: red; margin-right: 10px;">{{ noTaxAmountDifference }}</span>
</el-col>
</el-row>
<el-table v-loading="loading" :data="orderDetailList">
@@ -56,8 +36,6 @@
</el-table-column>
<el-table-column label="产品数量" align="center" prop="quantity" />
<el-table-column label="单位" align="center" prop="unit" />
<el-table-column label="含税单价" align="center" prop="taxPrice" />
<el-table-column label="无税单价" align="center" prop="noTaxPrice" />
<el-table-column label="备注" align="center" prop="remark" />
<el-table-column label="操作" align="center" width="200">
<template #default="scope">
@@ -81,13 +59,6 @@
<el-form-item label="单位" prop="unit">
<el-input v-model="form.unit" placeholder="单位" />
</el-form-item>
<el-form-item label="含税单价" prop="taxPrice">
<el-input-number :controls=false controls-position="right" v-model="form.taxPrice" placeholder="请输入含税单价" />
</el-form-item>
<el-form-item label="无税单价" prop="noTaxPrice">
<el-input-number :controls=false controls-position="right" v-model="form.noTaxPrice" placeholder="请输入无税单价"
:min="0" :max="form.taxPrice" />
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" placeholder="请输入备注" />
</el-form-item>
@@ -165,22 +136,6 @@ export default {
// 是否可以编辑(订单状态为新建时才能编辑)
canEdit() {
return this.orderInfo && this.orderInfo.orderStatus === EOrderStatus.NEW;
},
actualAmount() {
return this.orderDetailList?.reduce((total, item) => {
return total + (item.taxPrice || 0) * (item.quantity || 0);
}, 0) || 0;
},
amountDifference() {
return ((this.orderInfo?.taxAmount || 0) - this.actualAmount);
},
noTaxAmount() {
return this.orderDetailList?.reduce((total, item) => {
return total + (item.noTaxPrice || 0) * (item.quantity || 0);
}, 0) || 0;
},
noTaxAmountDifference() {
return ((this.orderInfo?.noTaxAmount || 0) - this.noTaxAmount);
}
},
watch: {
@@ -224,8 +179,6 @@ export default {
orderId: this.orderId,
productId: undefined,
quantity: undefined,
taxPrice: undefined,
noTaxPrice: undefined,
unit: undefined,
remark: undefined,
delFlag: undefined,

File diff suppressed because it is too large Load Diff

View File

@@ -94,8 +94,9 @@
<!-- 添加或修改应收款管理宽松版对话框 -->
<el-dialog :title="title" v-model="open" width="500px" append-to-body>
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="客户ID" prop="customerId">
<CustomerSelect v-model="form.customerId" />
<el-form-item label="客户" prop="customerId">
<el-input v-if="fixedCustomer" :model-value="fixedCustomerLabel" disabled />
<CustomerSelect v-else v-model="form.customerId" />
</el-form-item>
<el-form-item label="到期日" prop="dueDate">
<el-date-picker clearable
@@ -141,6 +142,7 @@
<script>
import { listReceivable, getReceivable, delReceivable, addReceivable, updateReceivable, updatePaidAmount } from "@/api/finance/receivable";
import CustomerSelect from '@/components/CustomerSelect/index.vue';
import { getCustomer } from "@/api/oms/customer";
export default {
name: "Receivable",
@@ -151,6 +153,25 @@ export default {
orderId: {
type: [String, Number],
required: true
},
customerId: {
type: [String, Number],
required: false,
default: undefined
},
customerName: {
type: String,
required: false,
default: ""
}
},
computed: {
fixedCustomer() {
return this.customerId != null && String(this.customerId) !== ""
},
fixedCustomerLabel() {
const name = String(this.customerName || this.fixedCustomerName || "").trim()
return name || "—"
}
},
data() {
@@ -179,7 +200,7 @@ export default {
queryParams: {
pageNum: 1,
pageSize: 20,
customerId: undefined,
customerId: this.customerId != null && String(this.customerId) !== "" ? this.customerId : undefined,
orderId: this.orderId,
dueDate: undefined,
amount: undefined,
@@ -195,7 +216,8 @@ export default {
// 收款表单参数
receiveForm: {},
// 是否显示收款弹出层
receiveOpen: false
receiveOpen: false,
fixedCustomerName: ""
};
},
created() {
@@ -205,9 +227,35 @@ export default {
orderId(newVal) {
this.queryParams.orderId = newVal;
this.getList();
},
customerId(newVal) {
this.queryParams.customerId = newVal != null && String(newVal) !== "" ? newVal : undefined
this.loadFixedCustomerName()
},
customerName() {
this.loadFixedCustomerName()
}
},
methods: {
loadFixedCustomerName() {
if (!this.fixedCustomer) {
this.fixedCustomerName = ""
return
}
const name = String(this.customerName || "").trim()
if (name) {
this.fixedCustomerName = name
return
}
const id = this.customerId
if (id == null || String(id) === "") return
getCustomer(id).then(res => {
const d = res && res.data ? res.data : null
this.fixedCustomerName = (d && d.name) ? String(d.name) : ""
}).catch(() => {
this.fixedCustomerName = ""
})
},
/** 查询应收款管理(宽松版)列表 */
getList() {
this.loading = true;
@@ -226,7 +274,7 @@ export default {
reset() {
this.form = {
receivableId: undefined,
customerId: undefined,
customerId: this.fixedCustomer ? this.customerId : undefined,
orderId: this.orderId,
dueDate: undefined,
amount: undefined,
@@ -263,6 +311,7 @@ export default {
this.reset();
this.open = true;
this.title = "添加应收款管理(宽松版)";
this.loadFixedCustomerName()
},
/** 修改按钮操作 */
handleUpdate(row) {
@@ -291,7 +340,11 @@ export default {
this.buttonLoading = false;
});
} else {
addReceivable(this.form).then(response => {
const payload = Object.assign({}, this.form, {
customerId: this.fixedCustomer ? this.customerId : this.form.customerId,
orderId: this.orderId
})
addReceivable(payload).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();

View File

@@ -0,0 +1,761 @@
<template>
<div class="app-container salesman-page">
<el-row :gutter="16">
<!-- 左侧销售员列表对齐示意图的左列表 + 右明细布局 -->
<el-col :span="6" class="salesman-left">
<el-form :model="queryParams" ref="queryRef" label-width="56px" size="small" class="left-filter">
<el-form-item label="姓名">
<el-input
v-model="queryParams.name"
clearable
placeholder="请输入销售员名称"
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryParams.status" clearable placeholder="数据状态">
<el-option label="正常" :value="0" />
<el-option label="停用" :value="1" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<div class="left-actions">
<el-button type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
<el-button plain icon="Refresh" @click="getSalesmanList">刷新</el-button>
</div>
<div class="left-list" v-loading="leftLoading">
<el-empty v-if="!salesmanList.length && !leftLoading" description="暂无销售员" />
<el-scrollbar v-else max-height="calc(100vh - 290px)">
<div
v-for="item in salesmanList"
:key="item.salesmanId"
class="salesman-item"
:class="{ active: item.salesmanId === selectedSalesmanId }"
@click="handleSelect(item)"
>
<div class="salesman-item__left">
<el-tag v-if="item.status === 0" size="small" type="success">正常</el-tag>
<el-tag v-else size="small" type="danger">停用</el-tag>
<span class="salesman-item__name">{{ item.name }}</span>
</div>
<div class="salesman-item__actions">
<el-button link type="primary" icon="Edit" @click.stop="handleUpdate(item)">修改</el-button>
<el-button link type="danger" icon="Delete" @click.stop="handleDelete(item)">删除</el-button>
</div>
</div>
</el-scrollbar>
</div>
</el-col>
<!-- 右侧跟进信息Tab 结构对齐示意图 -->
<el-col :span="18" class="salesman-right">
<el-card shadow="never">
<div class="right-title">
<span>销售员</span>
<span class="right-title__name">{{ selectedSalesmanName || "未选择" }}</span>
</div>
<el-tabs v-model="activeTab" class="right-tabs" @tab-change="handleTabChange">
<el-tab-pane label="跟进客户" name="followCustomer" />
<el-tab-pane label="跟进合同" name="followContract" />
<el-tab-pane label="发货单据" name="shippingDocs" />
<el-tab-pane label="生产成果" name="production" />
<el-tab-pane label="计划发货" name="planShipping" />
</el-tabs>
<div class="right-body">
<el-empty v-if="!selectedSalesmanId" description="请先从左侧选择销售员" />
<template v-else>
<div v-show="activeTab === 'followCustomer'">
<!-- 跟进客户从订单反查客户后端接口 /oa/salesman/{id}/customers -->
<el-table v-loading="customerLoading" :data="customerList">
<el-table-column label="公司名称" prop="name" min-width="220" />
<el-table-column label="联系方式" min-width="160">
<template #default="scope">
{{ scope.row.mobile || scope.row.telephone || "-" }}
</template>
</el-table-column>
<el-table-column label="税号" min-width="160">
<template #default>
-
</template>
</el-table-column>
<el-table-column label="行业" prop="industryId" width="120" />
<el-table-column label="客户等级" prop="level" width="120" />
<el-table-column label="地址" prop="detailAddress" min-width="260" show-overflow-tooltip />
</el-table>
<pagination
v-show="customerTotal > 0"
:total="customerTotal"
v-model:page="customerQuery.pageNum"
v-model:limit="customerQuery.pageSize"
@pagination="loadCustomers"
/>
</div>
<div v-show="activeTab === 'followContract'">
<!-- 跟进合同复用现有订单接口/oa/order/list以销售经理名称过滤 -->
<el-table v-loading="orderLoading" :data="orderList">
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="客户" prop="customerName" min-width="200" />
<el-table-column label="状态" prop="orderStatus" width="120" />
<el-table-column label="创建时间" prop="createTime" width="180" />
</el-table>
<pagination
v-show="orderTotal > 0"
:total="orderTotal"
v-model:page="orderQuery.pageNum"
v-model:limit="orderQuery.pageSize"
@pagination="loadOrders"
/>
</div>
<div v-show="activeTab === 'shippingDocs'">
<el-table v-loading="shippingLoading" :data="shippingList">
<el-table-column label="发货单号" prop="shippingNo" min-width="180" />
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="收货单位" prop="receiverCompany" min-width="220" />
<el-table-column label="发货时间" prop="shipTime" width="180" />
<el-table-column label="物流公司" prop="logisticsCompany" min-width="140" />
<el-table-column label="运单号" prop="logisticsNo" min-width="180" />
<el-table-column label="状态" prop="status" width="120" />
</el-table>
</div>
<div v-show="activeTab === 'production'">
<div class="production-wrap">
<div class="production-summary">
<div class="production-summary__item">订单数{{ productionSummary.orderCount }}</div>
<div class="production-summary__item">计划总量{{ formatQty(productionSummary.planQty) }}</div>
<div class="production-summary__item">已完成总量{{ formatQty(productionSummary.finishedQty) }}</div>
<div class="production-summary__item">完成率{{ productionSummary.rateText }}</div>
</div>
<el-table v-loading="productionLoading" :data="productionOrderSummaryList" size="small" border>
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="客户" prop="customerName" min-width="180" show-overflow-tooltip />
<el-table-column label="计划" prop="planQty" width="120" align="center" />
<el-table-column label="完成" prop="finishedQty" width="120" align="center" />
<el-table-column label="完成率" width="120" align="center">
<template #default="scope">
{{ formatRate(scope.row.finishedQty, scope.row.planQty) }}
</template>
</el-table-column>
<el-table-column label="最近更新" prop="lastUpdateTime" min-width="160" />
</el-table>
</div>
</div>
<div v-show="activeTab === 'planShipping'">
<el-card shadow="never">
<template #header>
<div class="plan-shipping__header">
计划发货
<span class="plan-shipping__sub">产品明细按该销售员全部订单汇总</span>
</div>
</template>
<div class="plan-shipping__summary">
<div class="plan-shipping__summary-item">总条数{{ planDetailSummary.totalLines }}</div>
<div class="plan-shipping__summary-item">总数量{{ planDetailSummary.totalQty }}</div>
<div class="plan-shipping__summary-item">订单数{{ planDetailSummary.totalOrders }}</div>
<div class="plan-shipping__summary-item">已发货订单{{ planDetailSummary.shippedOrders }}</div>
</div>
<el-table
v-loading="planDetailLoading"
:data="planOrderDetailList"
size="small"
border
>
<el-table-column label="订单编号" prop="orderCode" min-width="160" />
<el-table-column label="客户" prop="customerName" min-width="160" show-overflow-tooltip />
<el-table-column label="产品编号" min-width="140">
<template #default="scope">
<el-popover placement="right" trigger="hover" width="460">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="订单编号">{{ scope.row.orderCode || "-" }}</el-descriptions-item>
<el-descriptions-item label="客户">{{ scope.row.customerName || "-" }}</el-descriptions-item>
<el-descriptions-item label="产品编号">{{ scope.row.productCode || "-" }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ scope.row.productName || "-" }}</el-descriptions-item>
<el-descriptions-item label="规格">{{ scope.row.spec || "-" }}</el-descriptions-item>
<el-descriptions-item label="型号">{{ scope.row.model || "-" }}</el-descriptions-item>
<el-descriptions-item label="单位">{{ scope.row.unit || "-" }}</el-descriptions-item>
<el-descriptions-item label="数量">{{ scope.row.quantity ?? "-" }}</el-descriptions-item>
<el-descriptions-item label="备注" :span="2">{{ scope.row.remark || "-" }}</el-descriptions-item>
</el-descriptions>
<template #reference>
<el-link type="primary" :underline="false">{{ scope.row.productCode || "-" }}</el-link>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="产品名称" prop="productName" min-width="180" show-overflow-tooltip />
<el-table-column label="规格" prop="spec" min-width="140" show-overflow-tooltip />
<el-table-column label="型号" prop="model" min-width="140" show-overflow-tooltip />
<el-table-column label="单位" prop="unit" width="90" />
<el-table-column label="数量" prop="quantity" width="90" />
<el-table-column label="备注" prop="remark" min-width="160" show-overflow-tooltip />
<el-table-column label="发货状态" width="110">
<template #default="scope">
<el-tag v-if="scope.row.shippedFlag" type="success" size="small">已发货</el-tag>
<el-tag v-else type="info" size="small">未发货</el-tag>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
</div>
</el-card>
</el-col>
</el-row>
<!-- 新增/修改销售员对话框 -->
<el-dialog :title="dialogTitle" v-model="dialogOpen" width="520px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="90px">
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入销售员姓名" />
</el-form-item>
<el-form-item label="手机" prop="mobile">
<el-input v-model="form.mobile" placeholder="请输入联系电话" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="0">正常</el-radio>
<el-radio :value="1">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button :loading="submitLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="dialogOpen = false"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script>
import { listSalesman, addSalesman, updateSalesman, delSalesman, listSalesmanCustomers } from "@/api/oms/salesman";
import { listOrder } from "@/api/oms/order";
import { listShippingOrder } from "@/api/oms/shippingOrder";
import { listOrderDetail } from "@/api/oms/orderDetail";
import { listOrderProduction } from "@/api/oms/orderProduction";
export default {
name: "Salesman",
data() {
return {
leftLoading: false,
salesmanList: [],
selectedSalesmanId: undefined,
selectedSalesmanName: "",
activeTab: "followCustomer",
// 左侧查询条件(用于筛选销售员列表)
queryParams: {
pageNum: 1,
pageSize: 9999,
name: undefined,
status: undefined
},
// 跟进客户
customerLoading: false,
customerList: [],
customerTotal: 0,
customerQuery: {
pageNum: 1,
pageSize: 20
},
// 跟进合同(订单列表)
orderLoading: false,
orderList: [],
orderTotal: 0,
orderQuery: {
pageNum: 1,
pageSize: 20
},
// 发货单据
shippingLoading: false,
shippingList: [],
// 生产成果:按订单汇总(只读)
productionLoading: false,
productionOrderSummaryList: [],
// 计划发货:产品明细(按销售员全部订单汇总)
planDetailLoading: false,
planOrderDetailList: [],
// 新增/修改弹窗
dialogOpen: false,
dialogTitle: "",
submitLoading: false,
form: {},
rules: {
name: [{ required: true, message: "请输入销售员姓名", trigger: "blur" }]
}
};
},
computed: {
planDetailSummary() {
const rows = this.planOrderDetailList || [];
const totalLines = rows.length;
const totalQty = rows.reduce((sum, r) => sum + Number(r && r.quantity != null ? r.quantity : 0), 0);
const totalOrders = new Set(rows.map(r => r && r.orderId).filter(v => v != null)).size;
const shippedOrders = new Set(rows.filter(r => r && r.shippedFlag).map(r => r.orderId).filter(v => v != null)).size;
return {
totalLines,
totalQty,
totalOrders,
shippedOrders
};
},
productionSummary() {
const list = Array.isArray(this.productionOrderSummaryList) ? this.productionOrderSummaryList : [];
const orderCount = list.length;
const planQty = list.reduce((sum, r) => sum + Number(r && r.planQty != null ? r.planQty : 0), 0);
const finishedQty = list.reduce((sum, r) => sum + Number(r && r.finishedQty != null ? r.finishedQty : 0), 0);
const rateText = planQty > 0 ? `${((finishedQty / planQty) * 100).toFixed(1)}%` : "0%";
return { orderCount, planQty, finishedQty, rateText };
}
},
created() {
this.getSalesmanList();
},
methods: {
// 查询销售员列表
getSalesmanList() {
this.leftLoading = true;
listSalesman(this.queryParams)
.then(res => {
this.salesmanList = (res && res.rows) ? res.rows : [];
// 列表刷新后:若未选中,则默认选中第一条,方便右侧直接看到内容
if (!this.selectedSalesmanId && this.salesmanList.length) {
this.handleSelect(this.salesmanList[0]);
}
})
.finally(() => {
this.leftLoading = false;
});
},
// 搜索
handleQuery() {
this.queryParams.pageNum = 1;
this.getSalesmanList();
},
// 重置
resetQuery() {
this.queryParams.name = undefined;
this.queryParams.status = undefined;
this.handleQuery();
},
// 选中销售员:刷新右侧数据
handleSelect(item) {
if (!item || !item.salesmanId) return;
this.selectedSalesmanId = item.salesmanId;
this.selectedSalesmanName = item.name;
this.customerQuery.pageNum = 1;
this.orderQuery.pageNum = 1;
this.loadCustomers();
if (this.activeTab === "followContract") {
this.loadOrders();
}
if (this.activeTab === "shippingDocs") {
this.loadShippingOrders();
}
if (this.activeTab === "production") {
this.loadProductionSummary();
}
if (this.activeTab === "planShipping") {
this.loadPlanShipping();
}
},
// Tab切换按需加载避免每次都拉所有数据
handleTabChange() {
if (!this.selectedSalesmanId) return;
if (this.activeTab === "followCustomer") {
this.loadCustomers();
}
if (this.activeTab === "followContract") {
this.loadOrders();
}
if (this.activeTab === "shippingDocs") {
this.loadShippingOrders();
}
if (this.activeTab === "production") {
this.loadProductionSummary();
}
if (this.activeTab === "planShipping") {
this.loadPlanShipping();
}
},
// 跟进客户:后端反查客户
loadCustomers() {
if (!this.selectedSalesmanId) return;
this.customerLoading = true;
listSalesmanCustomers(this.selectedSalesmanId, this.customerQuery)
.then(res => {
this.customerList = (res && res.rows) ? res.rows : [];
this.customerTotal = res && typeof res.total === "number" ? res.total : 0;
})
.finally(() => {
this.customerLoading = false;
});
},
// 跟进合同:按 salesManager姓名过滤订单
loadOrders() {
if (!this.selectedSalesmanId) return;
this.orderLoading = true;
listOrder({
...this.orderQuery,
salesmanId: this.selectedSalesmanId
})
.then(res => {
this.orderList = (res && res.rows) ? res.rows : [];
this.orderTotal = res && typeof res.total === "number" ? res.total : 0;
})
.finally(() => {
this.orderLoading = false;
});
},
loadShippingOrders() {
if (!this.selectedSalesmanId) return;
this.shippingLoading = true;
listShippingOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 })
.then(res => {
this.shippingList = (res && res.rows) ? res.rows : [];
})
.finally(() => {
this.shippingLoading = false;
});
},
async loadProductionSummary() {
if (!this.selectedSalesmanId) return;
this.productionLoading = true;
try {
const orderRes = await listOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 });
const orders = (orderRes && orderRes.rows) ? orderRes.rows : [];
const promises = orders
.filter(o => o && o.orderId != null)
.map(async (o) => {
const prodRes = await listOrderProduction({ orderId: o.orderId, pageNum: 1, pageSize: 9999 });
const rows = (prodRes && prodRes.rows) ? prodRes.rows : [];
const planQty = rows.reduce((sum, r) => sum + Number(r && r.planQty != null ? r.planQty : 0), 0);
const finishedQty = rows.reduce((sum, r) => sum + Number(r && r.finishedQty != null ? r.finishedQty : 0), 0);
let lastUpdateTime = "";
rows.forEach(r => {
const t = r && r.updateTime ? String(r.updateTime) : "";
if (t && (!lastUpdateTime || new Date(t).getTime() > new Date(lastUpdateTime).getTime())) {
lastUpdateTime = t;
}
});
return {
orderId: o.orderId,
orderCode: o.orderCode,
customerName: o.customerName,
planQty,
finishedQty,
lastUpdateTime
};
});
this.productionOrderSummaryList = await Promise.all(promises);
} finally {
this.productionLoading = false;
}
},
loadPlanShipping() {
if (!this.selectedSalesmanId) return;
this.planDetailLoading = true;
Promise.all([
listOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 }),
listShippingOrder({ salesmanId: this.selectedSalesmanId, pageNum: 1, pageSize: 9999 })
])
.then(async ([orderRes, shipRes]) => {
const orders = (orderRes && orderRes.rows) ? orderRes.rows : [];
const shipping = (shipRes && shipRes.rows) ? shipRes.rows : [];
const orderMap = new Map();
orders.forEach(o => {
if (o && o.orderId != null) orderMap.set(o.orderId, o);
});
const shipStatusMap = new Map();
shipping.forEach(s => {
const orderId = s && s.orderId != null ? s.orderId : null;
if (orderId == null) return;
const n = Number(s && s.status != null ? s.status : 0);
const prev = shipStatusMap.has(orderId) ? shipStatusMap.get(orderId) : 0;
shipStatusMap.set(orderId, Number.isFinite(n) ? Math.max(prev, n) : prev);
});
const detailPromises = orders
.filter(o => o && o.orderId)
.map(o =>
listOrderDetail({ orderId: o.orderId, pageNum: 1, pageSize: 9999 })
.then(res => ({
order: o,
rows: (res && res.rows) ? res.rows : []
}))
);
const detailResults = await Promise.all(detailPromises);
const merged = [];
detailResults.forEach(({ order, rows }) => {
const maxStatus = shipStatusMap.has(order.orderId) ? shipStatusMap.get(order.orderId) : 0;
const shippedFlag = Number(maxStatus) >= 2;
rows
.filter(r => String(r && r.productType ? r.productType : "").toLowerCase() === "product")
.forEach(r => {
merged.push({
...r,
orderId: order.orderId,
orderCode: order.orderCode,
customerName: order.customerName,
shippedFlag
});
});
});
this.planOrderDetailList = merged;
})
.finally(() => {
this.planDetailLoading = false;
});
},
formatQty(v) {
const n = Number(v);
const val = Number.isFinite(n) ? n : 0;
return val.toFixed(2);
},
formatRate(finished, plan) {
const a = Number(finished);
const b = Number(plan);
const fa = Number.isFinite(a) ? a : 0;
const fb = Number.isFinite(b) ? b : 0;
if (fb <= 0) return "0%";
return `${((fa / fb) * 100).toFixed(1)}%`;
},
// 新增
handleAdd() {
this.dialogTitle = "新增销售员";
this.form = {
salesmanId: undefined,
name: "",
mobile: "",
status: 0,
remark: ""
};
this.dialogOpen = true;
},
// 修改
handleUpdate(row) {
this.dialogTitle = "修改销售员";
this.form = { ...row };
if (this.form.status === undefined || this.form.status === null) {
this.form.status = 0;
}
this.dialogOpen = true;
},
// 删除
handleDelete(row) {
this.$modal
.confirm(`是否确认删除销售员"${row.name}"`)
.then(() => delSalesman([row.salesmanId]))
.then(() => {
this.$modal.msgSuccess("删除成功");
// 删除后清空选中,避免右侧仍显示已删除人员的数据
if (this.selectedSalesmanId === row.salesmanId) {
this.selectedSalesmanId = undefined;
this.selectedSalesmanName = "";
}
this.getSalesmanList();
})
.catch(() => {});
},
// 提交新增/修改
submitForm() {
this.$refs["formRef"].validate(valid => {
if (!valid) return;
this.submitLoading = true;
const req = this.form.salesmanId ? updateSalesman(this.form) : addSalesman(this.form);
req
.then(() => {
this.$modal.msgSuccess("保存成功");
this.dialogOpen = false;
this.getSalesmanList();
})
.finally(() => {
this.submitLoading = false;
});
});
}
}
};
</script>
<style scoped>
.salesman-left {
height: calc(100vh - 110px);
}
.left-filter {
padding: 10px 10px 0 10px;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
background: #fff;
}
.left-actions {
display: flex;
gap: 8px;
margin: 10px 0;
}
.left-list {
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
background: #fff;
padding: 10px;
}
.salesman-item {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
padding: 10px;
margin-bottom: 10px;
cursor: pointer;
}
.salesman-item.active {
border-color: #409eff;
background: var(--el-color-primary-light-9);
}
.salesman-item__left {
display: flex;
align-items: center;
gap: 10px;
}
.salesman-item__name {
font-weight: 600;
color: #303133;
}
.salesman-item__actions {
display: flex;
align-items: center;
gap: 8px;
}
.right-title {
display: flex;
align-items: center;
gap: 6px;
padding-bottom: 8px;
border-bottom: 1px solid var(--el-border-color-light);
margin-bottom: 10px;
}
.right-title__name {
font-weight: 600;
color: #303133;
}
.right-body {
padding-top: 14px;
}
:deep(.right-tabs .el-tabs__content) {
display: none;
}
:deep(.right-tabs .el-tabs__header) {
margin: 0;
}
.plan-shipping__header {
display: flex;
align-items: center;
gap: 10px;
font-weight: 600;
color: #303133;
}
.plan-shipping__sub {
font-weight: 400;
color: #909399;
font-size: 12px;
}
.plan-shipping__summary {
display: flex;
align-items: center;
gap: 24px;
padding: 10px 12px;
margin-bottom: 10px;
background: #f5f7fa;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
color: #303133;
}
.plan-shipping__summary-item {
font-size: 13px;
}
.production-wrap {
padding-top: 6px;
}
.production-summary {
display: flex;
align-items: center;
gap: 24px;
padding: 10px 12px;
margin-bottom: 10px;
background: #f5f7fa;
border: 1px solid var(--el-border-color-light);
border-radius: 6px;
color: #303133;
}
.production-summary__item {
font-size: 13px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
-- ================================
-- 订单生产记录(按订单明细行维度)
-- 目标:用于“订单管理 -> 生产成果”记录每个订单明细行的计划/完成/不良等生产数据
-- 说明:
-- 1) 与 gear_order_detail 通过 order_detail_id 关联;与订单通过 order_id 关联
-- 2) init 接口会按订单明细自动生成缺失记录,并同步计划数量 plan_qty
-- 3) 逻辑删除使用 del_flag0存在 2删除
-- ================================
DROP TABLE IF EXISTS gear_order_production;
CREATE TABLE gear_order_production (
production_id bigint(20) NOT NULL COMMENT '生产记录ID',
order_id bigint(20) NOT NULL COMMENT '订单ID gear_order.order_id',
order_detail_id bigint(20) NOT NULL COMMENT '订单明细ID gear_order_detail.detail_id',
product_id bigint(20) DEFAULT NULL COMMENT '产品ID快照便于展示',
plan_qty decimal(16,4) NOT NULL DEFAULT 0 COMMENT '计划生产数量(通常=订单明细数量)',
finished_qty decimal(16,4) NOT NULL DEFAULT 0 COMMENT '累计完成数量',
bad_qty decimal(16,4) NOT NULL DEFAULT 0 COMMENT '不良数量',
unit varchar(32) DEFAULT '' COMMENT '单位',
remark varchar(500) DEFAULT NULL COMMENT '备注',
del_flag char(1) NOT NULL DEFAULT '0' COMMENT '删除标志0存在 2删除',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime COMMENT '创建时间',
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime COMMENT '更新时间',
PRIMARY KEY (production_id),
UNIQUE KEY uk_order_detail_id_del (order_detail_id, del_flag),
KEY idx_order_id (order_id),
KEY idx_product_id (product_id)
) ENGINE=InnoDB COMMENT='订单生产记录表';

View File

@@ -0,0 +1,68 @@
-- ================================
-- 发货单据(独立于出入库/WMS 单据)
-- 目标:用于“订单管理 -> 发货/发货单据”记录发货信息(发货单名称/收货单位/负责人/物流等)
-- 说明:
-- 1) 一张表存储发货单据主信息;与订单通过 order_id 关联
-- 2) 发货单号 shipping_no 后端自动生成(也可手填/改造)
-- 3) 逻辑删除使用 del_flag0存在 2删除
-- ================================
DROP TABLE IF EXISTS gear_shipping_order;
CREATE TABLE gear_shipping_order (
shipping_id bigint(20) NOT NULL COMMENT '发货单据ID',
shipping_no varchar(64) NOT NULL COMMENT '发货单号(唯一)',
shipping_name varchar(128) DEFAULT '' COMMENT '发货单名称(展示用)',
plan_id bigint(20) DEFAULT NULL COMMENT '发货计划ID gear_shipping_plan.plan_id',
order_id bigint(20) NOT NULL COMMENT '关联订单ID gear_order.order_id',
order_code varchar(64) DEFAULT '' COMMENT '订单编号快照',
receiver_company varchar(128) DEFAULT '' COMMENT '收货单位',
receiver_customer_id bigint(20) DEFAULT NULL COMMENT '收货客户ID gear_customer.customer_id',
ship_time datetime COMMENT '发货时间',
responsible_name varchar(64) DEFAULT '' COMMENT '负责人',
logistics_company varchar(64) DEFAULT '' COMMENT '物流公司',
logistics_no varchar(64) DEFAULT '' COMMENT '运单号',
receiver_name varchar(64) DEFAULT '' COMMENT '收货人',
receiver_phone varchar(32) DEFAULT '' COMMENT '收货电话',
receiver_address varchar(255) DEFAULT '' COMMENT '收货地址',
status char(1) NOT NULL DEFAULT '0' COMMENT '完成状态0未发货 1已打印 2已发货 3已完成',
remark varchar(500) DEFAULT NULL COMMENT '备注',
del_flag char(1) NOT NULL DEFAULT '0' COMMENT '删除标志0存在 2删除',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime COMMENT '创建时间',
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime COMMENT '更新时间',
PRIMARY KEY (shipping_id),
UNIQUE KEY uk_shipping_no (shipping_no),
KEY idx_plan_id (plan_id),
KEY idx_order_id (order_id),
KEY idx_shipping_name (shipping_name),
KEY idx_receiver_company (receiver_company),
KEY idx_receiver_customer_id (receiver_customer_id),
KEY idx_logistics_no (logistics_no),
KEY idx_ship_time (ship_time)
) ENGINE=InnoDB COMMENT='发货单据表';
-- ================================
-- 发货计划(独立表)
-- 目标用于“发货单据”左侧计划列表计划下可关联多张发货单据gear_shipping_order.plan_id
-- 说明:
-- 1) 一张表存储计划主信息(计划名称/计划日期/状态)
-- 2) 逻辑删除使用 del_flag0存在 2删除
-- ================================
DROP TABLE IF EXISTS gear_shipping_plan;
CREATE TABLE gear_shipping_plan (
plan_id bigint(20) NOT NULL COMMENT '发货计划ID',
plan_name varchar(128) NOT NULL COMMENT '计划名称',
plan_date date NOT NULL COMMENT '计划日期',
status char(1) NOT NULL DEFAULT '0' COMMENT '状态0未完成 1已完成',
remark varchar(500) DEFAULT NULL COMMENT '备注',
del_flag char(1) NOT NULL DEFAULT '0' COMMENT '删除标志0存在 2删除',
create_by varchar(64) DEFAULT '' COMMENT '创建者',
create_time datetime COMMENT '创建时间',
update_by varchar(64) DEFAULT '' COMMENT '更新者',
update_time datetime COMMENT '更新时间',
PRIMARY KEY (plan_id),
KEY idx_plan_date (plan_date),
KEY idx_plan_name (plan_name)
) ENGINE=InnoDB COMMENT='发货计划表';