feat(warehouse): 升级智能代理服务功能
- 集成大模型配置支持,包括API密钥、超时设置和输出令牌限制 - 优化意图解析算法,提升数量提取和引用识别准确性 - 增加入库单关联采购请求和到货单功能 - 完善入库确认前的库区和操作员验证机制 - 重构HTTP客户端配置,统一连接超时和协议版本设置 - 优化前端对话框组件,改进错误处理和结果显示 - 添加移动端上下文引用功能,增强对话连续性 - 扩展入库记录表格显示来源单号和待分配状态 - 实现Excel导入组件错误处理和更新选项控制
This commit is contained in:
parent
46ec60d6f5
commit
2ea0df3e1b
@ -0,0 +1,239 @@
|
|||||||
|
package com.ruoyi.web.controller.warehouse;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PutMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.ruoyi.common.annotation.Log;
|
||||||
|
import com.ruoyi.common.core.controller.BaseController;
|
||||||
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
|
import com.ruoyi.common.core.page.TableDataInfo;
|
||||||
|
import com.ruoyi.common.enums.BusinessType;
|
||||||
|
import com.ruoyi.common.utils.poi.ExcelUtil;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseBatch;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseBatchAssignRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrival;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrivalAcceptanceRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrivalRevokeRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseStatusUpdateRequest;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseArrivalService;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseBatchService;
|
||||||
|
import com.ruoyi.warehouse.service.IProductInfoService;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseRequestService;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseSmartImportService;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/warehouse/purchase")
|
||||||
|
public class PurchaseRequestController extends BaseController
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private IPurchaseRequestService purchaseRequestService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPurchaseBatchService purchaseBatchService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPurchaseArrivalService purchaseArrivalService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IProductInfoService productInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IPurchaseSmartImportService purchaseSmartImportService;
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:list,warehouse:purchase:manage')")
|
||||||
|
@GetMapping("/list")
|
||||||
|
public TableDataInfo list(PurchaseRequest purchaseRequest)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
return getDataTable(purchaseRequestService.selectPurchaseRequestList(purchaseRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:export,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购申请", businessType = BusinessType.EXPORT)
|
||||||
|
@PostMapping("/export")
|
||||||
|
public void export(HttpServletResponse response, PurchaseRequest purchaseRequest) throws Exception
|
||||||
|
{
|
||||||
|
List<PurchaseRequest> list = purchaseRequestService.selectPurchaseRequestList(purchaseRequest);
|
||||||
|
PurchaseTemplateExcelUtil.writeExport(response, list, productInfoService.selectProductInfoAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:import,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购申请", businessType = BusinessType.IMPORT)
|
||||||
|
@PostMapping("/importData")
|
||||||
|
public AjaxResult importData(MultipartFile file) throws Exception
|
||||||
|
{
|
||||||
|
ExcelUtil<PurchaseRequest> util = new ExcelUtil<PurchaseRequest>(PurchaseRequest.class);
|
||||||
|
List<PurchaseRequest> requestList = util.importExcel(file.getInputStream());
|
||||||
|
return success(purchaseRequestService.importPurchaseRequest(requestList, getUsername()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:add,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购申请智能导入", businessType = BusinessType.IMPORT)
|
||||||
|
@PostMapping("/smart-import")
|
||||||
|
public AjaxResult smartImport(MultipartFile file) throws Exception
|
||||||
|
{
|
||||||
|
return success(purchaseSmartImportService.analyze(file));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:import,warehouse:purchase:manage')")
|
||||||
|
@PostMapping("/importTemplate")
|
||||||
|
public void importTemplate(HttpServletResponse response) throws Exception
|
||||||
|
{
|
||||||
|
PurchaseTemplateExcelUtil.write(response, productInfoService.selectProductInfoAll());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:query,warehouse:purchase:manage')")
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public AjaxResult getInfo(@PathVariable Long id)
|
||||||
|
{
|
||||||
|
return success(purchaseRequestService.selectPurchaseRequestById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:add,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购申请", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping
|
||||||
|
public AjaxResult add(@Validated @RequestBody PurchaseRequest purchaseRequest)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseRequestService.insertPurchaseRequest(purchaseRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:add,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购申请表格录入", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping("/table")
|
||||||
|
public AjaxResult tableAdd(@RequestBody List<PurchaseRequest> requestList)
|
||||||
|
{
|
||||||
|
return success(purchaseRequestService.batchInsertPurchaseRequest(requestList));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购申请", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping
|
||||||
|
public AjaxResult edit(@Validated @RequestBody PurchaseRequest purchaseRequest)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseRequestService.updatePurchaseRequest(purchaseRequest));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购状态", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/{id}/status")
|
||||||
|
public AjaxResult updateStatus(@PathVariable Long id,
|
||||||
|
@Validated @RequestBody PurchaseStatusUpdateRequest request)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseRequestService.updatePurchaseStatus(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:list,warehouse:purchase:manage')")
|
||||||
|
@GetMapping("/arrival/{requestId}")
|
||||||
|
public AjaxResult arrivalList(@PathVariable Long requestId)
|
||||||
|
{
|
||||||
|
return success(purchaseArrivalService.selectPurchaseArrivalList(requestId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购到货", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping("/arrival")
|
||||||
|
public AjaxResult addArrival(@Validated @RequestBody PurchaseArrival arrival)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseArrivalService.insertPurchaseArrival(arrival));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购到货验收", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/arrival/{id}/accept")
|
||||||
|
public AjaxResult acceptArrival(@PathVariable Long id,
|
||||||
|
@Validated @RequestBody PurchaseArrivalAcceptanceRequest request)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseArrivalService.acceptPurchaseArrival(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:edit,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "撤销采购到货验收", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/arrival/{id}/revoke")
|
||||||
|
public AjaxResult revokeArrival(@PathVariable Long id,
|
||||||
|
@Validated @RequestBody PurchaseArrivalRevokeRequest request)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseArrivalService.revokePurchaseArrivalAcceptance(id, request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:remove,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购到货", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/arrival/{id}")
|
||||||
|
public AjaxResult removeArrival(@PathVariable Long id)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseArrivalService.deletePurchaseArrival(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')")
|
||||||
|
@GetMapping("/batch/list")
|
||||||
|
public TableDataInfo batchList(PurchaseBatch batch)
|
||||||
|
{
|
||||||
|
startPage();
|
||||||
|
return getDataTable(purchaseBatchService.selectPurchaseBatchList(batch));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')")
|
||||||
|
@GetMapping("/batch/options")
|
||||||
|
public AjaxResult batchOptions(String keyword)
|
||||||
|
{
|
||||||
|
return success(purchaseBatchService.selectPurchaseBatchOptions(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:list,warehouse:purchase:manage')")
|
||||||
|
@GetMapping("/batch/filter-options")
|
||||||
|
public AjaxResult batchFilterOptions(String keyword)
|
||||||
|
{
|
||||||
|
return success(purchaseBatchService.selectPurchaseBatchFilterOptions(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购批次", businessType = BusinessType.INSERT)
|
||||||
|
@PostMapping("/batch")
|
||||||
|
public AjaxResult addBatch(@RequestBody PurchaseBatch batch)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseBatchService.insertPurchaseBatch(batch));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购批次", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/batch")
|
||||||
|
public AjaxResult editBatch(@RequestBody PurchaseBatch batch)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseBatchService.updatePurchaseBatch(batch));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购批次分配", businessType = BusinessType.UPDATE)
|
||||||
|
@PutMapping("/batch/assign")
|
||||||
|
public AjaxResult assignBatch(@RequestBody PurchaseBatchAssignRequest request)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseBatchService.assignPurchaseBatch(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasPermi('warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购批次", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/batch/{id}")
|
||||||
|
public AjaxResult removeBatch(@PathVariable Long id)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseBatchService.deletePurchaseBatchById(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreAuthorize("@ss.hasAnyPermi('warehouse:purchase:remove,warehouse:purchase:manage')")
|
||||||
|
@Log(title = "采购申请", businessType = BusinessType.DELETE)
|
||||||
|
@DeleteMapping("/{ids}")
|
||||||
|
public AjaxResult remove(@PathVariable Long[] ids)
|
||||||
|
{
|
||||||
|
return toAjax(purchaseRequestService.deletePurchaseRequestByIds(ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,264 @@
|
|||||||
|
package com.ruoyi.web.controller.warehouse;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.apache.poi.ss.usermodel.Cell;
|
||||||
|
import org.apache.poi.ss.usermodel.CellStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.DataValidation;
|
||||||
|
import org.apache.poi.ss.usermodel.DataValidationConstraint;
|
||||||
|
import org.apache.poi.ss.usermodel.DataValidationHelper;
|
||||||
|
import org.apache.poi.ss.usermodel.DataValidation.ErrorStyle;
|
||||||
|
import org.apache.poi.ss.usermodel.FillPatternType;
|
||||||
|
import org.apache.poi.ss.usermodel.Font;
|
||||||
|
import org.apache.poi.ss.usermodel.IndexedColors;
|
||||||
|
import org.apache.poi.ss.usermodel.Name;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.SheetVisibility;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.common.utils.file.FileUtils;
|
||||||
|
import com.ruoyi.warehouse.domain.ProductInfo;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseRequest;
|
||||||
|
|
||||||
|
public final class PurchaseTemplateExcelUtil
|
||||||
|
{
|
||||||
|
private static final String[] HEADERS = {
|
||||||
|
"关联物料编码", "物料名称", "品牌", "类别", "参数规格", "采购链接",
|
||||||
|
"需求数量", "单位", "期望到货日期", "产品备注", "申请备注"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final String[] EXPORT_HEADERS = {
|
||||||
|
"关联物料编码", "物料名称", "品牌", "类别", "参数规格", "采购链接",
|
||||||
|
"需求数量", "单位", "期望到货日期", "申请人", "采购状态", "采购批次", "进度说明",
|
||||||
|
"产品备注", "申请备注", "已验收数量", "待验收数量"
|
||||||
|
};
|
||||||
|
|
||||||
|
private PurchaseTemplateExcelUtil()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void write(HttpServletResponse response, List<ProductInfo> products) throws IOException
|
||||||
|
{
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
FileUtils.setAttachmentResponseHeader(response, "采购申请导入模板.xlsx");
|
||||||
|
|
||||||
|
try (XSSFWorkbook workbook = createTemplateWorkbook(products))
|
||||||
|
{
|
||||||
|
workbook.write(response.getOutputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static XSSFWorkbook createTemplateWorkbook(List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
XSSFWorkbook workbook = new XSSFWorkbook();
|
||||||
|
Sheet sheet = workbook.createSheet("采购申请数据");
|
||||||
|
Sheet options = workbook.createSheet("系统下拉数据");
|
||||||
|
CellStyle headerStyle = createHeaderStyle(workbook);
|
||||||
|
Row header = sheet.createRow(0);
|
||||||
|
for (int index = 0; index < HEADERS.length; index++)
|
||||||
|
{
|
||||||
|
Cell cell = header.createCell(index);
|
||||||
|
cell.setCellValue(HEADERS[index]);
|
||||||
|
cell.setCellStyle(headerStyle);
|
||||||
|
sheet.setColumnWidth(index, columnWidth(index));
|
||||||
|
}
|
||||||
|
sheet.createFreezePane(0, 1);
|
||||||
|
|
||||||
|
addOptionColumn(workbook, sheet, options, 0, "物料编码", values(products, "code"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 1, "物料名称", values(products, "name"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 2, "品牌", values(products, "brand"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 3, "类别", values(products, "category"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 4, "参数规格", values(products, "spec"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 7, "单位", values(products, "unit"));
|
||||||
|
workbook.setSheetVisibility(workbook.getSheetIndex(options), SheetVisibility.HIDDEN);
|
||||||
|
return workbook;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void writeExport(HttpServletResponse response, List<PurchaseRequest> requests,
|
||||||
|
List<ProductInfo> products) throws IOException
|
||||||
|
{
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
FileUtils.setAttachmentResponseHeader(response, "采购申请数据.xlsx");
|
||||||
|
|
||||||
|
try (XSSFWorkbook workbook = new XSSFWorkbook())
|
||||||
|
{
|
||||||
|
Sheet sheet = workbook.createSheet("采购申请数据");
|
||||||
|
Sheet options = workbook.createSheet("系统下拉数据");
|
||||||
|
CellStyle headerStyle = createHeaderStyle(workbook);
|
||||||
|
Row header = sheet.createRow(0);
|
||||||
|
for (int index = 0; index < EXPORT_HEADERS.length; index++)
|
||||||
|
{
|
||||||
|
Cell cell = header.createCell(index);
|
||||||
|
cell.setCellValue(EXPORT_HEADERS[index]);
|
||||||
|
cell.setCellStyle(headerStyle);
|
||||||
|
sheet.setColumnWidth(index, exportColumnWidth(index));
|
||||||
|
}
|
||||||
|
sheet.createFreezePane(0, 1);
|
||||||
|
|
||||||
|
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
for (int rowIndex = 0; rowIndex < requests.size(); rowIndex++)
|
||||||
|
{
|
||||||
|
PurchaseRequest request = requests.get(rowIndex);
|
||||||
|
Row row = sheet.createRow(rowIndex + 1);
|
||||||
|
Object[] values = {
|
||||||
|
request.getProductCode(), request.getMaterialName(), request.getMaterialBrand(),
|
||||||
|
request.getCategory(), request.getMaterialParams(),
|
||||||
|
request.getPurchaseLink(), request.getQuantity(), request.getUnit(),
|
||||||
|
request.getExpectedDate() == null ? null : dateFormat.format(request.getExpectedDate()),
|
||||||
|
request.getApplicantName(), purchaseStatusLabel(request.getPurchaseStatus()),
|
||||||
|
request.getPurchaseBatchNo(), request.getProgressRemark(), request.getProductRemark(),
|
||||||
|
request.getRequestRemark(), request.getAcceptedQuantity(), request.getPendingArrivalQuantity()
|
||||||
|
};
|
||||||
|
for (int column = 0; column < values.length; column++)
|
||||||
|
{
|
||||||
|
setCellValue(row.createCell(column), values[column]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addOptionColumn(workbook, sheet, options, 0, "物料编码", values(products, "code"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 1, "物料名称", values(products, "name"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 2, "品牌", values(products, "brand"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 3, "类别", values(products, "category"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 4, "参数规格", values(products, "spec"));
|
||||||
|
addOptionColumn(workbook, sheet, options, 7, "单位", values(products, "unit"));
|
||||||
|
workbook.setSheetVisibility(workbook.getSheetIndex(options), SheetVisibility.HIDDEN);
|
||||||
|
workbook.write(response.getOutputStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addOptionColumn(XSSFWorkbook workbook, Sheet sheet, Sheet options,
|
||||||
|
int targetColumn, String title, List<String> values)
|
||||||
|
{
|
||||||
|
int optionColumn = options.getRow(0) == null ? 0 : options.getRow(0).getLastCellNum();
|
||||||
|
Row titleRow = options.getRow(0);
|
||||||
|
if (titleRow == null)
|
||||||
|
{
|
||||||
|
titleRow = options.createRow(0);
|
||||||
|
optionColumn = 0;
|
||||||
|
}
|
||||||
|
titleRow.createCell(optionColumn).setCellValue(title);
|
||||||
|
if (values.isEmpty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < values.size(); index++)
|
||||||
|
{
|
||||||
|
Row row = options.getRow(index + 1);
|
||||||
|
if (row == null)
|
||||||
|
{
|
||||||
|
row = options.createRow(index + 1);
|
||||||
|
}
|
||||||
|
row.createCell(optionColumn).setCellValue(values.get(index));
|
||||||
|
}
|
||||||
|
|
||||||
|
String rangeName = "purchase_option_" + targetColumn;
|
||||||
|
Name name = workbook.createName();
|
||||||
|
name.setNameName(rangeName);
|
||||||
|
String optionColumnName = org.apache.poi.ss.util.CellReference.convertNumToColString(optionColumn);
|
||||||
|
name.setRefersToFormula("'系统下拉数据'!$" + optionColumnName + "$2:$" + optionColumnName + "$" + (values.size() + 1));
|
||||||
|
|
||||||
|
DataValidationHelper helper = sheet.getDataValidationHelper();
|
||||||
|
DataValidationConstraint constraint = helper.createFormulaListConstraint(rangeName);
|
||||||
|
CellRangeAddressList range = new CellRangeAddressList(1, 10000, targetColumn, targetColumn);
|
||||||
|
DataValidation validation = helper.createValidation(constraint, range);
|
||||||
|
validation.setEmptyCellAllowed(true);
|
||||||
|
// XSSF writes the inverse OOXML showDropDown flag; true keeps the arrow visible in Excel/WPS.
|
||||||
|
validation.setSuppressDropDownArrow(true);
|
||||||
|
validation.setErrorStyle(ErrorStyle.WARNING);
|
||||||
|
validation.setShowErrorBox(true);
|
||||||
|
validation.createErrorBox("不在系统数据中", "可以继续输入新值,确认后系统会新增或复用该属性");
|
||||||
|
validation.setShowPromptBox(false);
|
||||||
|
sheet.addValidationData(validation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<String> values(List<ProductInfo> products, String field)
|
||||||
|
{
|
||||||
|
Set<String> values = new LinkedHashSet<>();
|
||||||
|
for (ProductInfo product : products)
|
||||||
|
{
|
||||||
|
String value = switch (field)
|
||||||
|
{
|
||||||
|
case "code" -> product.getProductCode();
|
||||||
|
case "name" -> product.getProductName();
|
||||||
|
case "brand" -> product.getBrand();
|
||||||
|
case "category" -> product.getCategory();
|
||||||
|
case "spec" -> product.getSpec();
|
||||||
|
case "unit" -> product.getUnit();
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
if (StringUtils.isNotBlank(value))
|
||||||
|
{
|
||||||
|
values.add(value.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CellStyle createHeaderStyle(XSSFWorkbook workbook)
|
||||||
|
{
|
||||||
|
CellStyle style = workbook.createCellStyle();
|
||||||
|
style.setFillForegroundColor(IndexedColors.LIGHT_CORNFLOWER_BLUE.getIndex());
|
||||||
|
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
|
||||||
|
Font font = workbook.createFont();
|
||||||
|
font.setBold(true);
|
||||||
|
style.setFont(font);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int columnWidth(int column)
|
||||||
|
{
|
||||||
|
if (column == 5)
|
||||||
|
{
|
||||||
|
return 42 * 256;
|
||||||
|
}
|
||||||
|
if (column == 4 || column >= 9)
|
||||||
|
{
|
||||||
|
return 24 * 256;
|
||||||
|
}
|
||||||
|
return 18 * 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int exportColumnWidth(int column)
|
||||||
|
{
|
||||||
|
if (column == 5)
|
||||||
|
{
|
||||||
|
return 42 * 256;
|
||||||
|
}
|
||||||
|
if (column == 4 || column == 12 || column == 13 || column == 14)
|
||||||
|
{
|
||||||
|
return 24 * 256;
|
||||||
|
}
|
||||||
|
return 18 * 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setCellValue(Cell cell, Object value)
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
cell.setBlank();
|
||||||
|
}
|
||||||
|
else if (value instanceof Number number)
|
||||||
|
{
|
||||||
|
cell.setCellValue(number.doubleValue());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.setCellValue(String.valueOf(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String purchaseStatusLabel(Integer status)
|
||||||
|
{
|
||||||
|
String[] labels = { "待采购", "已下单", "采购中", "部分到货", "已到齐", "已取消" };
|
||||||
|
return status != null && status >= 0 && status < labels.length ? labels[status] : "未知";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -144,6 +144,13 @@ xss:
|
|||||||
|
|
||||||
# WMS agent model config. Keep api-key in environment variables, not in source code.
|
# WMS agent model config. Keep api-key in environment variables, not in source code.
|
||||||
agent:
|
agent:
|
||||||
|
model:
|
||||||
|
base-url: ${AGENT_MODEL_BASE_URL:http://192.168.28.10:14081}
|
||||||
|
api-key: ${AGENT_MODEL_API_KEY:}
|
||||||
|
model: ${AGENT_MODEL_NAME:Qwen/Qwen3.8-27B-FP8}
|
||||||
|
timeout-ms: ${AGENT_MODEL_TIMEOUT_MS:8000}
|
||||||
|
max-output-tokens: ${AGENT_MODEL_MAX_OUTPUT_TOKENS:512}
|
||||||
|
# 图片批量导入仍使用支持视觉输入的远程模型。
|
||||||
openai:
|
openai:
|
||||||
base-url: ${OPENAI_BASE_URL:http://192.168.28.10:18080}
|
base-url: ${OPENAI_BASE_URL:http://192.168.28.10:18080}
|
||||||
api-key: ${OPENAI_API_KEY:sk-dd185946191d42eaa54892dec488bf7b54cad2d7433aee8aac7a017365b0829d}
|
api-key: ${OPENAI_API_KEY:sk-dd185946191d42eaa54892dec488bf7b54cad2d7433aee8aac7a017365b0829d}
|
||||||
|
|||||||
@ -9,6 +9,8 @@ import org.slf4j.LoggerFactory;
|
|||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
import com.ruoyi.common.constant.CacheConstants;
|
import com.ruoyi.common.constant.CacheConstants;
|
||||||
import com.ruoyi.common.constant.Constants;
|
import com.ruoyi.common.constant.Constants;
|
||||||
import com.ruoyi.common.core.domain.model.LoginUser;
|
import com.ruoyi.common.core.domain.model.LoginUser;
|
||||||
@ -72,8 +74,7 @@ public class TokenService
|
|||||||
// 解析对应的权限以及用户信息
|
// 解析对应的权限以及用户信息
|
||||||
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
String uuid = (String) claims.get(Constants.LOGIN_USER_KEY);
|
||||||
String userKey = getTokenKey(uuid);
|
String userKey = getTokenKey(uuid);
|
||||||
LoginUser user = redisCache.getCacheObject(userKey);
|
return getCachedLoginUser(userKey);
|
||||||
return user;
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -248,7 +249,7 @@ public class TokenService
|
|||||||
}
|
}
|
||||||
for (String key : keys)
|
for (String key : keys)
|
||||||
{
|
{
|
||||||
LoginUser loginUser = redisCache.getCacheObject(key);
|
LoginUser loginUser = getCachedLoginUser(key);
|
||||||
if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin())
|
if (loginUser == null || loginUser.getUser() == null || loginUser.getUser().isAdmin())
|
||||||
{
|
{
|
||||||
// 管理员拥有所有权限,跳过
|
// 管理员拥有所有权限,跳过
|
||||||
@ -267,4 +268,33 @@ public class TokenService
|
|||||||
log.info("角色[{}]权限变更,已刷新在线用户[{}]的权限缓存", roleId, loginUser.getUsername());
|
log.info("角色[{}]权限变更,已刷新在线用户[{}]的权限缓存", roleId, loginUser.getUsername());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DevTools 重启后,旧 Redis 会话可能由不同类加载器反序列化为 JSONObject。
|
||||||
|
*/
|
||||||
|
private LoginUser getCachedLoginUser(String userKey)
|
||||||
|
{
|
||||||
|
Object cached = redisCache.getCacheObject(userKey);
|
||||||
|
if (cached == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (cached instanceof LoginUser loginUser)
|
||||||
|
{
|
||||||
|
return loginUser;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (cached instanceof JSONObject jsonObject)
|
||||||
|
{
|
||||||
|
return jsonObject.toJavaObject(LoginUser.class);
|
||||||
|
}
|
||||||
|
return JSON.parseObject(JSON.toJSONString(cached), LoginUser.class);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
log.warn("Redis 登录会话[{}]格式无效,已忽略:{}", userKey, e.getMessage());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,13 @@ public class InRecord extends BaseEntity
|
|||||||
|
|
||||||
private Long productId;
|
private Long productId;
|
||||||
|
|
||||||
|
private Long purchaseRequestId;
|
||||||
|
|
||||||
|
private Long arrivalId;
|
||||||
|
|
||||||
|
@Excel(name = "来源单号")
|
||||||
|
private String sourceNo;
|
||||||
|
|
||||||
@Excel(name = "入库类型", readConverterExp = "1=生产入库,2=采购入库,3=归还入库,4=退货入库")
|
@Excel(name = "入库类型", readConverterExp = "1=生产入库,2=采购入库,3=归还入库,4=退货入库")
|
||||||
private Integer inType;
|
private Integer inType;
|
||||||
|
|
||||||
@ -75,6 +82,12 @@ public class InRecord extends BaseEntity
|
|||||||
@NotNull(message = "产品不能为空")
|
@NotNull(message = "产品不能为空")
|
||||||
public Long getProductId() { return productId; }
|
public Long getProductId() { return productId; }
|
||||||
public void setProductId(Long productId) { this.productId = productId; }
|
public void setProductId(Long productId) { this.productId = productId; }
|
||||||
|
public Long getPurchaseRequestId() { return purchaseRequestId; }
|
||||||
|
public void setPurchaseRequestId(Long purchaseRequestId) { this.purchaseRequestId = purchaseRequestId; }
|
||||||
|
public Long getArrivalId() { return arrivalId; }
|
||||||
|
public void setArrivalId(Long arrivalId) { this.arrivalId = arrivalId; }
|
||||||
|
public String getSourceNo() { return sourceNo; }
|
||||||
|
public void setSourceNo(String sourceNo) { this.sourceNo = sourceNo; }
|
||||||
|
|
||||||
@NotNull(message = "入库类型不能为空")
|
@NotNull(message = "入库类型不能为空")
|
||||||
public Integer getInType() { return inType; }
|
public Integer getInType() { return inType; }
|
||||||
|
|||||||
@ -0,0 +1,96 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public class PurchaseArrival extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String arrivalNo;
|
||||||
|
private Long purchaseRequestId;
|
||||||
|
private BigDecimal arrivalQuantity;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
private Date arrivalDate;
|
||||||
|
|
||||||
|
private Integer acceptanceStatus;
|
||||||
|
private String acceptanceRemark;
|
||||||
|
private String acceptedBy;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date acceptedTime;
|
||||||
|
|
||||||
|
private Integer preAcceptPurchaseStatus;
|
||||||
|
private String revokedBy;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
|
private Date revokedTime;
|
||||||
|
|
||||||
|
private String revokeRemark;
|
||||||
|
|
||||||
|
private String requestNo;
|
||||||
|
private Long applicantId;
|
||||||
|
private String applicantName;
|
||||||
|
private Long productId;
|
||||||
|
private String productCode;
|
||||||
|
private String materialName;
|
||||||
|
private String materialBrand;
|
||||||
|
private String materialParams;
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getArrivalNo() { return arrivalNo; }
|
||||||
|
public void setArrivalNo(String arrivalNo) { this.arrivalNo = arrivalNo; }
|
||||||
|
public Long getPurchaseRequestId() { return purchaseRequestId; }
|
||||||
|
public void setPurchaseRequestId(Long purchaseRequestId) { this.purchaseRequestId = purchaseRequestId; }
|
||||||
|
|
||||||
|
@NotNull(message = "到货数量不能为空")
|
||||||
|
@DecimalMin(value = "0.001", message = "到货数量必须大于0")
|
||||||
|
public BigDecimal getArrivalQuantity() { return arrivalQuantity; }
|
||||||
|
public void setArrivalQuantity(BigDecimal arrivalQuantity) { this.arrivalQuantity = arrivalQuantity; }
|
||||||
|
|
||||||
|
@NotNull(message = "到货日期不能为空")
|
||||||
|
public Date getArrivalDate() { return arrivalDate; }
|
||||||
|
public void setArrivalDate(Date arrivalDate) { this.arrivalDate = arrivalDate; }
|
||||||
|
public Integer getAcceptanceStatus() { return acceptanceStatus; }
|
||||||
|
public void setAcceptanceStatus(Integer acceptanceStatus) { this.acceptanceStatus = acceptanceStatus; }
|
||||||
|
public String getAcceptanceRemark() { return acceptanceRemark; }
|
||||||
|
public void setAcceptanceRemark(String acceptanceRemark) { this.acceptanceRemark = acceptanceRemark; }
|
||||||
|
public String getAcceptedBy() { return acceptedBy; }
|
||||||
|
public void setAcceptedBy(String acceptedBy) { this.acceptedBy = acceptedBy; }
|
||||||
|
public Date getAcceptedTime() { return acceptedTime; }
|
||||||
|
public void setAcceptedTime(Date acceptedTime) { this.acceptedTime = acceptedTime; }
|
||||||
|
public Integer getPreAcceptPurchaseStatus() { return preAcceptPurchaseStatus; }
|
||||||
|
public void setPreAcceptPurchaseStatus(Integer preAcceptPurchaseStatus) { this.preAcceptPurchaseStatus = preAcceptPurchaseStatus; }
|
||||||
|
public String getRevokedBy() { return revokedBy; }
|
||||||
|
public void setRevokedBy(String revokedBy) { this.revokedBy = revokedBy; }
|
||||||
|
public Date getRevokedTime() { return revokedTime; }
|
||||||
|
public void setRevokedTime(Date revokedTime) { this.revokedTime = revokedTime; }
|
||||||
|
public String getRevokeRemark() { return revokeRemark; }
|
||||||
|
public void setRevokeRemark(String revokeRemark) { this.revokeRemark = revokeRemark; }
|
||||||
|
public String getRequestNo() { return requestNo; }
|
||||||
|
public void setRequestNo(String requestNo) { this.requestNo = requestNo; }
|
||||||
|
public Long getApplicantId() { return applicantId; }
|
||||||
|
public void setApplicantId(Long applicantId) { this.applicantId = applicantId; }
|
||||||
|
public String getApplicantName() { return applicantName; }
|
||||||
|
public void setApplicantName(String applicantName) { this.applicantName = applicantName; }
|
||||||
|
public Long getProductId() { return productId; }
|
||||||
|
public void setProductId(Long productId) { this.productId = productId; }
|
||||||
|
public String getProductCode() { return productCode; }
|
||||||
|
public void setProductCode(String productCode) { this.productCode = productCode; }
|
||||||
|
public String getMaterialName() { return materialName; }
|
||||||
|
public void setMaterialName(String materialName) { this.materialName = materialName; }
|
||||||
|
public String getMaterialBrand() { return materialBrand; }
|
||||||
|
public void setMaterialBrand(String materialBrand) { this.materialBrand = materialBrand; }
|
||||||
|
public String getMaterialParams() { return materialParams; }
|
||||||
|
public void setMaterialParams(String materialParams) { this.materialParams = materialParams; }
|
||||||
|
public String getUnit() { return unit; }
|
||||||
|
public void setUnit(String unit) { this.unit = unit; }
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public class PurchaseArrivalAcceptanceRequest
|
||||||
|
{
|
||||||
|
@NotNull(message = "验收结果不能为空")
|
||||||
|
private Integer acceptanceStatus;
|
||||||
|
private String acceptanceRemark;
|
||||||
|
|
||||||
|
public Integer getAcceptanceStatus() { return acceptanceStatus; }
|
||||||
|
public void setAcceptanceStatus(Integer acceptanceStatus) { this.acceptanceStatus = acceptanceStatus; }
|
||||||
|
public String getAcceptanceRemark() { return acceptanceRemark; }
|
||||||
|
public void setAcceptanceRemark(String acceptanceRemark) { this.acceptanceRemark = acceptanceRemark; }
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
|
||||||
|
public class PurchaseArrivalRevokeRequest
|
||||||
|
{
|
||||||
|
private String revokeRemark;
|
||||||
|
|
||||||
|
@NotBlank(message = "撤销原因不能为空")
|
||||||
|
public String getRevokeRemark()
|
||||||
|
{
|
||||||
|
return revokeRemark;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRevokeRemark(String revokeRemark)
|
||||||
|
{
|
||||||
|
this.revokeRemark = revokeRemark;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,28 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
|
||||||
|
public class PurchaseBatch extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
private String batchNo;
|
||||||
|
private String batchName;
|
||||||
|
private Integer purchaseStatus;
|
||||||
|
private String progressRemark;
|
||||||
|
private Integer requestCount;
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getBatchNo() { return batchNo; }
|
||||||
|
public void setBatchNo(String batchNo) { this.batchNo = batchNo; }
|
||||||
|
public String getBatchName() { return batchName; }
|
||||||
|
public void setBatchName(String batchName) { this.batchName = batchName; }
|
||||||
|
public Integer getPurchaseStatus() { return purchaseStatus; }
|
||||||
|
public void setPurchaseStatus(Integer purchaseStatus) { this.purchaseStatus = purchaseStatus; }
|
||||||
|
public String getProgressRemark() { return progressRemark; }
|
||||||
|
public void setProgressRemark(String progressRemark) { this.progressRemark = progressRemark; }
|
||||||
|
public Integer getRequestCount() { return requestCount; }
|
||||||
|
public void setRequestCount(Integer requestCount) { this.requestCount = requestCount; }
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
public class PurchaseBatchAssignRequest
|
||||||
|
{
|
||||||
|
private Long[] requestIds;
|
||||||
|
private Long batchId;
|
||||||
|
|
||||||
|
public Long[] getRequestIds() { return requestIds; }
|
||||||
|
public void setRequestIds(Long[] requestIds) { this.requestIds = requestIds; }
|
||||||
|
public Long getBatchId() { return batchId; }
|
||||||
|
public void setBatchId(Long batchId) { this.batchId = batchId; }
|
||||||
|
}
|
||||||
@ -0,0 +1,144 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Date;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.ruoyi.common.annotation.Excel;
|
||||||
|
import com.ruoyi.common.core.domain.BaseEntity;
|
||||||
|
import jakarta.validation.constraints.DecimalMin;
|
||||||
|
import jakarta.validation.constraints.NotBlank;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public class PurchaseRequest extends BaseEntity
|
||||||
|
{
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
private String requestNo;
|
||||||
|
|
||||||
|
private Long productId;
|
||||||
|
|
||||||
|
private Long purchaseBatchId;
|
||||||
|
|
||||||
|
@Excel(name = "关联物料编码", prompt = "已有物料建议填写准确编码;留空时按名称、品牌、类别、参数规格匹配")
|
||||||
|
private String productCode;
|
||||||
|
|
||||||
|
@Excel(name = "物料名称", prompt = "必填;无匹配物料时将自动新增产品信息")
|
||||||
|
private String materialName;
|
||||||
|
|
||||||
|
@Excel(name = "品牌")
|
||||||
|
private String materialBrand;
|
||||||
|
|
||||||
|
@Excel(name = "类别")
|
||||||
|
private String category;
|
||||||
|
|
||||||
|
@Excel(name = "参数规格")
|
||||||
|
private String materialParams;
|
||||||
|
|
||||||
|
@Excel(name = "采购链接")
|
||||||
|
private String purchaseLink;
|
||||||
|
|
||||||
|
@Excel(name = "需求数量")
|
||||||
|
private BigDecimal quantity;
|
||||||
|
|
||||||
|
@Excel(name = "单位")
|
||||||
|
private String unit;
|
||||||
|
|
||||||
|
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||||
|
@Excel(name = "期望到货日期", width = 20, dateFormat = "yyyy-MM-dd")
|
||||||
|
private Date expectedDate;
|
||||||
|
|
||||||
|
private Long applicantId;
|
||||||
|
|
||||||
|
@Excel(name = "申请人", type = Excel.Type.EXPORT)
|
||||||
|
private String applicantName;
|
||||||
|
|
||||||
|
@Excel(name = "采购状态", readConverterExp = "0=待采购,1=已下单,2=采购中,3=部分到货,4=已到齐,5=已取消", type = Excel.Type.EXPORT)
|
||||||
|
private Integer purchaseStatus;
|
||||||
|
|
||||||
|
@Excel(name = "采购批次", type = Excel.Type.EXPORT)
|
||||||
|
private String purchaseBatchNo;
|
||||||
|
|
||||||
|
@Excel(name = "进度说明", type = Excel.Type.EXPORT)
|
||||||
|
private String progressRemark;
|
||||||
|
|
||||||
|
@Excel(name = "产品备注")
|
||||||
|
private String productRemark;
|
||||||
|
|
||||||
|
@Excel(name = "申请备注")
|
||||||
|
private String requestRemark;
|
||||||
|
|
||||||
|
private String productName;
|
||||||
|
private String productBrand;
|
||||||
|
private String productCategory;
|
||||||
|
private String productSpec;
|
||||||
|
|
||||||
|
@Excel(name = "已验收数量", type = Excel.Type.EXPORT)
|
||||||
|
private BigDecimal acceptedQuantity;
|
||||||
|
|
||||||
|
@Excel(name = "待验收数量", type = Excel.Type.EXPORT)
|
||||||
|
private BigDecimal pendingArrivalQuantity;
|
||||||
|
|
||||||
|
private BigDecimal remainingQuantity;
|
||||||
|
|
||||||
|
public Long getId() { return id; }
|
||||||
|
public void setId(Long id) { this.id = id; }
|
||||||
|
public String getRequestNo() { return requestNo; }
|
||||||
|
public void setRequestNo(String requestNo) { this.requestNo = requestNo; }
|
||||||
|
public Long getProductId() { return productId; }
|
||||||
|
public void setProductId(Long productId) { this.productId = productId; }
|
||||||
|
public Long getPurchaseBatchId() { return purchaseBatchId; }
|
||||||
|
public void setPurchaseBatchId(Long purchaseBatchId) { this.purchaseBatchId = purchaseBatchId; }
|
||||||
|
public String getProductCode() { return productCode; }
|
||||||
|
public void setProductCode(String productCode) { this.productCode = productCode; }
|
||||||
|
|
||||||
|
@NotBlank(message = "物料名称不能为空")
|
||||||
|
public String getMaterialName() { return materialName; }
|
||||||
|
public void setMaterialName(String materialName) { this.materialName = materialName; }
|
||||||
|
public String getMaterialBrand() { return materialBrand; }
|
||||||
|
public void setMaterialBrand(String materialBrand) { this.materialBrand = materialBrand; }
|
||||||
|
public String getCategory() { return category; }
|
||||||
|
public void setCategory(String category) { this.category = category; }
|
||||||
|
public String getMaterialParams() { return materialParams; }
|
||||||
|
public void setMaterialParams(String materialParams) { this.materialParams = materialParams; }
|
||||||
|
public String getPurchaseLink() { return purchaseLink; }
|
||||||
|
public void setPurchaseLink(String purchaseLink) { this.purchaseLink = purchaseLink; }
|
||||||
|
|
||||||
|
@NotNull(message = "需求数量不能为空")
|
||||||
|
@DecimalMin(value = "0.001", message = "需求数量必须大于0")
|
||||||
|
public BigDecimal getQuantity() { return quantity; }
|
||||||
|
public void setQuantity(BigDecimal quantity) { this.quantity = quantity; }
|
||||||
|
public String getUnit() { return unit; }
|
||||||
|
public void setUnit(String unit) { this.unit = unit; }
|
||||||
|
public Date getExpectedDate() { return expectedDate; }
|
||||||
|
public void setExpectedDate(Date expectedDate) { this.expectedDate = expectedDate; }
|
||||||
|
public Long getApplicantId() { return applicantId; }
|
||||||
|
public void setApplicantId(Long applicantId) { this.applicantId = applicantId; }
|
||||||
|
public String getApplicantName() { return applicantName; }
|
||||||
|
public void setApplicantName(String applicantName) { this.applicantName = applicantName; }
|
||||||
|
public Integer getPurchaseStatus() { return purchaseStatus; }
|
||||||
|
public void setPurchaseStatus(Integer purchaseStatus) { this.purchaseStatus = purchaseStatus; }
|
||||||
|
public String getPurchaseBatchNo() { return purchaseBatchNo; }
|
||||||
|
public void setPurchaseBatchNo(String purchaseBatchNo) { this.purchaseBatchNo = purchaseBatchNo; }
|
||||||
|
public String getProgressRemark() { return progressRemark; }
|
||||||
|
public void setProgressRemark(String progressRemark) { this.progressRemark = progressRemark; }
|
||||||
|
public String getProductRemark() { return productRemark; }
|
||||||
|
public void setProductRemark(String productRemark) { this.productRemark = productRemark; }
|
||||||
|
public String getRequestRemark() { return requestRemark; }
|
||||||
|
public void setRequestRemark(String requestRemark) { this.requestRemark = requestRemark; }
|
||||||
|
public String getProductName() { return productName; }
|
||||||
|
public void setProductName(String productName) { this.productName = productName; }
|
||||||
|
public String getProductBrand() { return productBrand; }
|
||||||
|
public void setProductBrand(String productBrand) { this.productBrand = productBrand; }
|
||||||
|
public String getProductCategory() { return productCategory; }
|
||||||
|
public void setProductCategory(String productCategory) { this.productCategory = productCategory; }
|
||||||
|
public String getProductSpec() { return productSpec; }
|
||||||
|
public void setProductSpec(String productSpec) { this.productSpec = productSpec; }
|
||||||
|
public BigDecimal getAcceptedQuantity() { return acceptedQuantity; }
|
||||||
|
public void setAcceptedQuantity(BigDecimal acceptedQuantity) { this.acceptedQuantity = acceptedQuantity; }
|
||||||
|
public BigDecimal getPendingArrivalQuantity() { return pendingArrivalQuantity; }
|
||||||
|
public void setPendingArrivalQuantity(BigDecimal pendingArrivalQuantity) { this.pendingArrivalQuantity = pendingArrivalQuantity; }
|
||||||
|
public BigDecimal getRemainingQuantity() { return remainingQuantity; }
|
||||||
|
public void setRemainingQuantity(BigDecimal remainingQuantity) { this.remainingQuantity = remainingQuantity; }
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.ruoyi.warehouse.domain;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public class PurchaseStatusUpdateRequest
|
||||||
|
{
|
||||||
|
@NotNull(message = "采购状态不能为空")
|
||||||
|
private Integer purchaseStatus;
|
||||||
|
private String progressRemark;
|
||||||
|
|
||||||
|
public Integer getPurchaseStatus() { return purchaseStatus; }
|
||||||
|
public void setPurchaseStatus(Integer purchaseStatus) { this.purchaseStatus = purchaseStatus; }
|
||||||
|
public String getProgressRemark() { return progressRemark; }
|
||||||
|
public void setProgressRemark(String progressRemark) { this.progressRemark = progressRemark; }
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@ public interface InRecordMapper
|
|||||||
|
|
||||||
public InRecord selectInRecordByIdForUpdate(Long id);
|
public InRecord selectInRecordByIdForUpdate(Long id);
|
||||||
|
|
||||||
|
public InRecord selectInRecordByArrivalIdForUpdate(Long arrivalId);
|
||||||
|
|
||||||
public InRecord checkInNoUnique(String inNo);
|
public InRecord checkInNoUnique(String inNo);
|
||||||
|
|
||||||
public int insertInRecord(InRecord inRecord);
|
public int insertInRecord(InRecord inRecord);
|
||||||
@ -20,4 +22,6 @@ public interface InRecordMapper
|
|||||||
public int confirmInRecord(InRecord inRecord);
|
public int confirmInRecord(InRecord inRecord);
|
||||||
|
|
||||||
public int deleteInRecordByIds(Long[] ids);
|
public int deleteInRecordByIds(Long[] ids);
|
||||||
|
|
||||||
|
public int deletePendingInRecordByArrivalId(Long arrivalId);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.ruoyi.warehouse.mapper;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrival;
|
||||||
|
|
||||||
|
public interface PurchaseArrivalMapper
|
||||||
|
{
|
||||||
|
List<PurchaseArrival> selectPurchaseArrivalList(Long purchaseRequestId);
|
||||||
|
PurchaseArrival selectPurchaseArrivalById(Long id);
|
||||||
|
PurchaseArrival selectPurchaseArrivalByIdForUpdate(Long id);
|
||||||
|
BigDecimal selectActiveQuantityByRequestId(Long purchaseRequestId);
|
||||||
|
BigDecimal selectAcceptedQuantityByRequestId(Long purchaseRequestId);
|
||||||
|
int countByRequestId(Long purchaseRequestId);
|
||||||
|
int insertPurchaseArrival(PurchaseArrival arrival);
|
||||||
|
int updatePurchaseArrivalAcceptance(PurchaseArrival arrival);
|
||||||
|
int revokePurchaseArrivalAcceptance(PurchaseArrival arrival);
|
||||||
|
int deletePendingPurchaseArrival(Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.ruoyi.warehouse.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseBatch;
|
||||||
|
|
||||||
|
public interface PurchaseBatchMapper
|
||||||
|
{
|
||||||
|
List<PurchaseBatch> selectPurchaseBatchList(PurchaseBatch batch);
|
||||||
|
List<PurchaseBatch> selectPurchaseBatchOptions(@Param("keyword") String keyword);
|
||||||
|
List<PurchaseBatch> selectPurchaseBatchFilterOptions(@Param("keyword") String keyword);
|
||||||
|
PurchaseBatch selectPurchaseBatchById(Long id);
|
||||||
|
PurchaseBatch selectPurchaseBatchByNo(String batchNo);
|
||||||
|
int insertPurchaseBatch(PurchaseBatch batch);
|
||||||
|
int updatePurchaseBatch(PurchaseBatch batch);
|
||||||
|
int countRequestsByBatchId(Long batchId);
|
||||||
|
int countRequestsByIds(Long[] requestIds);
|
||||||
|
int assignRequests(@Param("requestIds") Long[] requestIds, @Param("batch") PurchaseBatch batch, @Param("updateBy") String updateBy);
|
||||||
|
int syncRequestBatchNo(@Param("batchId") Long batchId, @Param("batchNo") String batchNo,
|
||||||
|
@Param("updateBy") String updateBy);
|
||||||
|
int deletePurchaseBatchById(Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
package com.ruoyi.warehouse.mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseRequest;
|
||||||
|
|
||||||
|
public interface PurchaseRequestMapper
|
||||||
|
{
|
||||||
|
List<PurchaseRequest> selectPurchaseRequestList(PurchaseRequest purchaseRequest);
|
||||||
|
PurchaseRequest selectPurchaseRequestById(@Param("id") Long id, @Param("applicantId") Long applicantId);
|
||||||
|
PurchaseRequest selectPurchaseRequestByIdForUpdate(Long id);
|
||||||
|
PurchaseRequest checkRequestNoUnique(String requestNo);
|
||||||
|
int insertPurchaseRequest(PurchaseRequest purchaseRequest);
|
||||||
|
int updatePurchaseRequest(@Param("purchaseRequest") PurchaseRequest purchaseRequest, @Param("applicantId") Long applicantId);
|
||||||
|
int deletePurchaseRequestByIds(@Param("ids") Long[] ids, @Param("applicantId") Long applicantId);
|
||||||
|
int updatePurchaseStatus(@Param("id") Long id, @Param("purchaseStatus") Integer purchaseStatus,
|
||||||
|
@Param("progressRemark") String progressRemark, @Param("updateBy") String updateBy);
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.ruoyi.warehouse.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrival;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrivalAcceptanceRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrivalRevokeRequest;
|
||||||
|
|
||||||
|
public interface IPurchaseArrivalService
|
||||||
|
{
|
||||||
|
List<PurchaseArrival> selectPurchaseArrivalList(Long purchaseRequestId);
|
||||||
|
int insertPurchaseArrival(PurchaseArrival arrival);
|
||||||
|
int acceptPurchaseArrival(Long id, PurchaseArrivalAcceptanceRequest request);
|
||||||
|
int revokePurchaseArrivalAcceptance(Long id, PurchaseArrivalRevokeRequest request);
|
||||||
|
int deletePurchaseArrival(Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.ruoyi.warehouse.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseBatch;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseBatchAssignRequest;
|
||||||
|
|
||||||
|
public interface IPurchaseBatchService
|
||||||
|
{
|
||||||
|
List<PurchaseBatch> selectPurchaseBatchList(PurchaseBatch batch);
|
||||||
|
List<PurchaseBatch> selectPurchaseBatchOptions(String keyword);
|
||||||
|
List<PurchaseBatch> selectPurchaseBatchFilterOptions(String keyword);
|
||||||
|
int insertPurchaseBatch(PurchaseBatch batch);
|
||||||
|
int updatePurchaseBatch(PurchaseBatch batch);
|
||||||
|
int assignPurchaseBatch(PurchaseBatchAssignRequest request);
|
||||||
|
int deletePurchaseBatchById(Long id);
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.ruoyi.warehouse.service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseStatusUpdateRequest;
|
||||||
|
|
||||||
|
public interface IPurchaseRequestService
|
||||||
|
{
|
||||||
|
List<PurchaseRequest> selectPurchaseRequestList(PurchaseRequest purchaseRequest);
|
||||||
|
PurchaseRequest selectPurchaseRequestById(Long id);
|
||||||
|
int insertPurchaseRequest(PurchaseRequest purchaseRequest);
|
||||||
|
int updatePurchaseRequest(PurchaseRequest purchaseRequest);
|
||||||
|
int deletePurchaseRequestByIds(Long[] ids);
|
||||||
|
int updatePurchaseStatus(Long id, PurchaseStatusUpdateRequest request);
|
||||||
|
int batchInsertPurchaseRequest(List<PurchaseRequest> requestList);
|
||||||
|
String importPurchaseRequest(List<PurchaseRequest> requestList, String username);
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.ruoyi.warehouse.service;
|
||||||
|
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
|
||||||
|
public interface IPurchaseSmartImportService
|
||||||
|
{
|
||||||
|
JSONObject analyze(MultipartFile file) throws Exception;
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import java.net.http.HttpClient;
|
|||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.text.Normalizer;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@ -46,6 +47,11 @@ import com.ruoyi.warehouse.service.IWarehouseAreaService;
|
|||||||
@Service
|
@Service
|
||||||
public class AgentServiceImpl implements IAgentService
|
public class AgentServiceImpl implements IAgentService
|
||||||
{
|
{
|
||||||
|
private static final HttpClient MODEL_CLIENT = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(3))
|
||||||
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
|
.build();
|
||||||
|
|
||||||
private static final String ACTION_QUERY_STOCK = "query_stock";
|
private static final String ACTION_QUERY_STOCK = "query_stock";
|
||||||
private static final String ACTION_CREATE_IN = "create_in_record";
|
private static final String ACTION_CREATE_IN = "create_in_record";
|
||||||
private static final String ACTION_CONFIRM_IN = "confirm_in_record";
|
private static final String ACTION_CONFIRM_IN = "confirm_in_record";
|
||||||
@ -55,19 +61,33 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
private static final String ACTION_CREATE_AREA = "create_area";
|
private static final String ACTION_CREATE_AREA = "create_area";
|
||||||
private static final String ACTION_UNKNOWN = "unknown";
|
private static final String ACTION_UNKNOWN = "unknown";
|
||||||
|
|
||||||
private static final Pattern NUMBER_PATTERN = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)");
|
|
||||||
private static final Pattern IN_NO_PATTERN = Pattern.compile("(IN\\d+)", Pattern.CASE_INSENSITIVE);
|
private static final Pattern IN_NO_PATTERN = Pattern.compile("(IN\\d+)", Pattern.CASE_INSENSITIVE);
|
||||||
private static final Pattern INDEX_PATTERN = Pattern.compile("第\\s*([0-9一二两三四五六七八九十]+)\\s*(个|条|项)?");
|
private static final Pattern INDEX_PATTERN = Pattern.compile("第\\s*([0-9一二两三四五六七八九十]+)\\s*(个|条|项)?");
|
||||||
|
private static final Pattern REFERENCE_PATTERN = Pattern.compile("上一个|上一条|刚才(?:的)?(?:那个|那条|那项)|刚刚(?:的)?(?:那个|那条|那项)|这个|那个|它|上述|上面(?:的)?");
|
||||||
|
private static final Pattern QUANTITY_WITH_UNIT_PATTERN = Pattern.compile("([0-9]+(?:\\.[0-9]+)?)\\s*(个|件|箱|支|台|套|包|卷|瓶|袋|张|本)");
|
||||||
|
private static final Pattern CHINESE_QUANTITY_PATTERN = Pattern.compile("([一二两三四五六七八九十]+)\\s*(个|件|箱|支|台|套|包|卷|瓶|袋|张|本)");
|
||||||
|
private static final Pattern ACTION_QUANTITY_PATTERN = Pattern.compile("(?:入库|出库|盘库|盘点)\\s*(?:数量\\s*[::]?\\s*)?([0-9]+(?:\\.[0-9]+)?)");
|
||||||
|
private static final Pattern NAMED_QUANTITY_PATTERN = Pattern.compile("数量\\s*[::]?\\s*([0-9]+(?:\\.[0-9]+)?)");
|
||||||
|
private static final Pattern PORT_SPEC_PATTERN = Pattern.compile("([0-90-9]+)\\s*(?:进|入)\\s*([0-90-9]+)\\s*出", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
@Value("${agent.openai.base-url}")
|
@Value("${agent.model.base-url}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
||||||
@Value("${agent.openai.api-key}")
|
@Value("${agent.model.api-key:}")
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
|
||||||
@Value("${agent.openai.model}")
|
@Value("${agent.model.model}")
|
||||||
private String model;
|
private String model;
|
||||||
|
|
||||||
|
@Value("${agent.model.timeout-ms:8000}")
|
||||||
|
private long modelTimeoutMs;
|
||||||
|
|
||||||
|
@Value("${agent.model.max-output-tokens:512}")
|
||||||
|
private int maxOutputTokens;
|
||||||
|
|
||||||
|
private volatile List<String> categoryCache = List.of();
|
||||||
|
private volatile long categoryCacheExpiresAt;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IWarehouseAreaService areaService;
|
private IWarehouseAreaService areaService;
|
||||||
|
|
||||||
@ -99,15 +119,15 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
|
|
||||||
AgentPlan plan = null;
|
AgentPlan plan = null;
|
||||||
List<String> warnings = new ArrayList<>();
|
List<String> warnings = new ArrayList<>();
|
||||||
if (StringUtils.isBlank(apiKey))
|
if (StringUtils.isBlank(baseUrl))
|
||||||
{
|
{
|
||||||
warnings.add("未配置 OPENAI_API_KEY,已使用基础规则解析");
|
warnings.add("未配置大模型地址,已使用基础规则解析");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
plan = callModel(text, contextResults);
|
plan = callModel(text, hasExplicitReference(text) ? contextResults : null);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -247,10 +267,13 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
{
|
{
|
||||||
plan.setAction(ACTION_UNKNOWN);
|
plan.setAction(ACTION_UNKNOWN);
|
||||||
}
|
}
|
||||||
if (requiresOperator(plan.getAction()) && StringUtils.isBlank(plan.getOperatorName()))
|
resolveExplicitAction(plan, originalText);
|
||||||
|
resolveOperator(plan, originalText, username);
|
||||||
|
|
||||||
|
if (requiresOperator(plan.getAction()) && originalText != null)
|
||||||
{
|
{
|
||||||
plan.setOperatorName(resolveNickName(username));
|
// 数量必须来自本次原文的明确数量表达,不能使用模型从规格中猜出的数字。
|
||||||
plan.getWarnings().add("未识别到人员,默认使用当前登录用户");
|
plan.setQuantity(extractQuantity(originalText));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ACTION_CREATE_IN.equals(plan.getAction()) && plan.getInType() == null)
|
if (ACTION_CREATE_IN.equals(plan.getAction()) && plan.getInType() == null)
|
||||||
@ -373,25 +396,27 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
{
|
{
|
||||||
JSONObject body = new JSONObject();
|
JSONObject body = new JSONObject();
|
||||||
body.put("model", model);
|
body.put("model", model);
|
||||||
JSONArray input = new JSONArray();
|
JSONArray messages = new JSONArray();
|
||||||
input.add(message("system", systemPrompt()));
|
messages.add(message("system", systemPrompt()));
|
||||||
input.add(message("user", buildUserInput(text, contextResults)));
|
messages.add(message("user", buildUserInput(text, contextResults)));
|
||||||
body.put("input", input);
|
body.put("messages", messages);
|
||||||
JSONObject format = new JSONObject();
|
body.put("response_format", JSONObject.of("type", "json_object"));
|
||||||
format.put("type", "json_object");
|
body.put("chat_template_kwargs", JSONObject.of("enable_thinking", false));
|
||||||
JSONObject textFormat = new JSONObject();
|
body.put("temperature", 0);
|
||||||
textFormat.put("format", format);
|
body.put("max_tokens", maxOutputTokens);
|
||||||
body.put("text", textFormat);
|
body.put("stream", false);
|
||||||
|
|
||||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(normalizeEndpoint(baseUrl)))
|
.uri(URI.create(normalizeEndpoint(baseUrl)))
|
||||||
.timeout(Duration.ofSeconds(30))
|
.timeout(Duration.ofMillis(modelTimeoutMs))
|
||||||
.header("Authorization", "Bearer " + apiKey)
|
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8))
|
.POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8));
|
||||||
.build();
|
if (StringUtils.isNotBlank(apiKey))
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
{
|
||||||
|
requestBuilder.header("Authorization", "Bearer " + apiKey);
|
||||||
|
}
|
||||||
|
HttpResponse<String> response = MODEL_CLIENT.send(requestBuilder.build(),
|
||||||
|
HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
if (response.statusCode() < 200 || response.statusCode() >= 300)
|
if (response.statusCode() < 200 || response.statusCode() >= 300)
|
||||||
{
|
{
|
||||||
throw new ServiceException("HTTP " + response.statusCode());
|
throw new ServiceException("HTTP " + response.statusCode());
|
||||||
@ -417,7 +442,9 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
return "你是库存管理系统的意图解析器,只返回一个 JSON 对象,不要解释。"
|
return "你是库存管理系统的意图解析器,只返回一个 JSON 对象,不要解释。"
|
||||||
+ "action 只能是 query_stock, create_in_record, confirm_in_record, create_out_record, create_stock_check, create_product, create_area, unknown。"
|
+ "action 只能是 query_stock, create_in_record, confirm_in_record, create_out_record, create_stock_check, create_product, create_area, unknown。"
|
||||||
+ "字段包括 action, areaName, productName, brand, category, spec, unit, quantity, inType, outType, operatorName, recordNo, remark, summary, referenceIndex。"
|
+ "字段包括 action, areaName, productName, brand, category, spec, unit, quantity, inType, outType, operatorName, recordNo, remark, summary, referenceIndex。"
|
||||||
+ "如果用户说第几个、第二个、第2条等,referenceIndex 返回从 1 开始的序号。"
|
+ "只有原文明说第几个、第2条、上一个、刚才那个、这个、那个或它时,referenceIndex 才能返回从 1 开始的序号;‘一个’、‘1个’表示数量,不是引用,没有明确指代必须返回 null。"
|
||||||
|
+ "quantity 只表示本次入库、出库或盘点的数量,规格型号里的数字不算数量,‘一个’表示数量 1。"
|
||||||
|
+ "类似‘2进6出’、‘8口’的描述优先放入 spec,不要把人员姓名、‘东西’、‘产品’放入 productName。"
|
||||||
+ "入库类型 inType:1生产入库,2采购入库,3归还入库,4退货入库。"
|
+ "入库类型 inType:1生产入库,2采购入库,3归还入库,4退货入库。"
|
||||||
+ "出库类型 outType:1生产出库,2退货出库,3销售出库。"
|
+ "出库类型 outType:1生产出库,2退货出库,3销售出库。"
|
||||||
+ "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;如果用户未提供 category,优先从 existingCategories 选择合适类别,不合适时推断一个简短合理的类别。"
|
+ "新增产品时尽量从用户输入提取 productName, brand, category, spec, unit;如果用户未提供 category,优先从 existingCategories 选择合适类别,不合适时推断一个简短合理的类别。"
|
||||||
@ -435,25 +462,40 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
|
|
||||||
private String normalizeEndpoint(String configuredBaseUrl)
|
private String normalizeEndpoint(String configuredBaseUrl)
|
||||||
{
|
{
|
||||||
String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:18080").trim();
|
String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:14081").trim();
|
||||||
while (url.endsWith("/"))
|
while (url.endsWith("/"))
|
||||||
{
|
{
|
||||||
url = url.substring(0, url.length() - 1);
|
url = url.substring(0, url.length() - 1);
|
||||||
}
|
}
|
||||||
if (url.endsWith("/v1"))
|
if (url.endsWith("/v1"))
|
||||||
{
|
{
|
||||||
return url + "/responses";
|
return url + "/chat/completions";
|
||||||
}
|
}
|
||||||
if (url.endsWith("/v1/responses"))
|
if (url.endsWith("/v1/chat/completions") || url.endsWith("/chat/completions"))
|
||||||
{
|
{
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
return url + "/v1/responses";
|
if (url.endsWith("/v1/responses"))
|
||||||
|
{
|
||||||
|
return url.substring(0, url.length() - "/responses".length()) + "/chat/completions";
|
||||||
|
}
|
||||||
|
return url + "/v1/chat/completions";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractOutputText(String responseBody)
|
private String extractOutputText(String responseBody)
|
||||||
{
|
{
|
||||||
JSONObject root = JSON.parseObject(responseBody);
|
JSONObject root = JSON.parseObject(responseBody);
|
||||||
|
JSONArray choices = root.getJSONArray("choices");
|
||||||
|
if (choices != null && !choices.isEmpty())
|
||||||
|
{
|
||||||
|
JSONObject choice = choices.getJSONObject(0);
|
||||||
|
JSONObject responseMessage = choice == null ? null : choice.getJSONObject("message");
|
||||||
|
String content = responseMessage == null ? null : responseMessage.getString("content");
|
||||||
|
if (StringUtils.isNotBlank(content))
|
||||||
|
{
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
}
|
||||||
String outputText = root.getString("output_text");
|
String outputText = root.getString("output_text");
|
||||||
if (StringUtils.isNotBlank(outputText))
|
if (StringUtils.isNotBlank(outputText))
|
||||||
{
|
{
|
||||||
@ -704,17 +746,35 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
|
|
||||||
private BigDecimal extractQuantity(String text)
|
private BigDecimal extractQuantity(String text)
|
||||||
{
|
{
|
||||||
Matcher matcher = NUMBER_PATTERN.matcher(StringUtils.defaultString(text));
|
String source = StringUtils.defaultString(text);
|
||||||
BigDecimal quantity = null;
|
Matcher matcher = NAMED_QUANTITY_PATTERN.matcher(source);
|
||||||
|
if (matcher.find())
|
||||||
|
{
|
||||||
|
return new BigDecimal(matcher.group(1));
|
||||||
|
}
|
||||||
|
matcher = QUANTITY_WITH_UNIT_PATTERN.matcher(source);
|
||||||
while (matcher.find())
|
while (matcher.find())
|
||||||
{
|
{
|
||||||
if (matcher.start() > 0 && text.charAt(matcher.start() - 1) == '第')
|
if (matcher.start() == 0 || source.charAt(matcher.start() - 1) != '第')
|
||||||
{
|
{
|
||||||
continue;
|
return new BigDecimal(matcher.group(1));
|
||||||
}
|
}
|
||||||
quantity = new BigDecimal(matcher.group(1));
|
|
||||||
}
|
}
|
||||||
return quantity;
|
matcher = CHINESE_QUANTITY_PATTERN.matcher(source);
|
||||||
|
while (matcher.find())
|
||||||
|
{
|
||||||
|
if (matcher.start() == 0 || source.charAt(matcher.start() - 1) != '第')
|
||||||
|
{
|
||||||
|
Integer value = parseIndex(matcher.group(1));
|
||||||
|
return value == null ? null : BigDecimal.valueOf(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matcher = ACTION_QUANTITY_PATTERN.matcher(source);
|
||||||
|
if (matcher.find())
|
||||||
|
{
|
||||||
|
return new BigDecimal(matcher.group(1));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractInNo(String text)
|
private String extractInNo(String text)
|
||||||
@ -865,12 +925,29 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
|
|
||||||
private void resolveReferencedResult(AgentPlan plan, String originalText, List<AgentResultItem> contextResults)
|
private void resolveReferencedResult(AgentPlan plan, String originalText, List<AgentResultItem> contextResults)
|
||||||
{
|
{
|
||||||
if (plan.getReferenceIndex() == null)
|
if (originalText == null)
|
||||||
{
|
{
|
||||||
plan.setReferenceIndex(extractReferenceIndex(originalText));
|
return;
|
||||||
}
|
}
|
||||||
if (plan.getReferenceIndex() == null)
|
Integer explicitIndex = extractReferenceIndex(originalText);
|
||||||
|
if (!hasExplicitReference(originalText))
|
||||||
{
|
{
|
||||||
|
// 不信任模型自行推断的序号,防止把“出库一个”理解成“出库第一个”。
|
||||||
|
plan.setReferenceIndex(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (explicitIndex != null)
|
||||||
|
{
|
||||||
|
plan.setReferenceIndex(explicitIndex);
|
||||||
|
}
|
||||||
|
else if (contextResults != null && contextResults.size() == 1)
|
||||||
|
{
|
||||||
|
plan.setReferenceIndex(1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
plan.setReferenceIndex(null);
|
||||||
|
plan.setMessage("上次结果有多项,请明确说第几个");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (contextResults == null || contextResults.isEmpty())
|
if (contextResults == null || contextResults.isEmpty())
|
||||||
@ -992,6 +1069,25 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
}
|
}
|
||||||
|
|
||||||
private List<String> existingCategories()
|
private List<String> existingCategories()
|
||||||
|
{
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
if (now < categoryCacheExpiresAt)
|
||||||
|
{
|
||||||
|
return categoryCache;
|
||||||
|
}
|
||||||
|
synchronized (this)
|
||||||
|
{
|
||||||
|
if (now < categoryCacheExpiresAt)
|
||||||
|
{
|
||||||
|
return categoryCache;
|
||||||
|
}
|
||||||
|
categoryCache = loadExistingCategories();
|
||||||
|
categoryCacheExpiresAt = now + 60_000L;
|
||||||
|
return categoryCache;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> loadExistingCategories()
|
||||||
{
|
{
|
||||||
Set<String> categories = new LinkedHashSet<>();
|
Set<String> categories = new LinkedHashSet<>();
|
||||||
for (ProductInfo product : productService.selectProductInfoAll())
|
for (ProductInfo product : productService.selectProductInfoAll())
|
||||||
@ -1016,6 +1112,94 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
|| ACTION_CREATE_CHECK.equals(action);
|
|| ACTION_CREATE_CHECK.equals(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean hasExplicitReference(String text)
|
||||||
|
{
|
||||||
|
return extractReferenceIndex(text) != null || REFERENCE_PATTERN.matcher(StringUtils.defaultString(text)).find();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resolveExplicitAction(AgentPlan plan, String originalText)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(originalText))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((originalText.contains("添加") || originalText.contains("新增") || originalText.contains("录入"))
|
||||||
|
&& originalText.contains("库区"))
|
||||||
|
{
|
||||||
|
plan.setAction(ACTION_CREATE_AREA);
|
||||||
|
}
|
||||||
|
else if ((originalText.contains("添加") || originalText.contains("新增") || originalText.contains("录入"))
|
||||||
|
&& (originalText.contains("产品") || originalText.contains("物料")))
|
||||||
|
{
|
||||||
|
plan.setAction(ACTION_CREATE_PRODUCT);
|
||||||
|
}
|
||||||
|
else if (originalText.contains("确认") && originalText.contains("入库"))
|
||||||
|
{
|
||||||
|
plan.setAction(ACTION_CONFIRM_IN);
|
||||||
|
}
|
||||||
|
else if (originalText.contains("出库"))
|
||||||
|
{
|
||||||
|
plan.setAction(ACTION_CREATE_OUT);
|
||||||
|
}
|
||||||
|
else if (originalText.contains("盘库") || originalText.contains("盘点"))
|
||||||
|
{
|
||||||
|
plan.setAction(ACTION_CREATE_CHECK);
|
||||||
|
}
|
||||||
|
else if (originalText.contains("入库"))
|
||||||
|
{
|
||||||
|
plan.setAction(ACTION_CREATE_IN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resolveOperator(AgentPlan plan, String originalText, String username)
|
||||||
|
{
|
||||||
|
if (!requiresOperator(plan.getAction()))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(originalText))
|
||||||
|
{
|
||||||
|
SysUser matchedUser = null;
|
||||||
|
int matchedLength = 0;
|
||||||
|
SysUser query = new SysUser();
|
||||||
|
query.setStatus("0");
|
||||||
|
for (SysUser user : userService.selectUserList(query))
|
||||||
|
{
|
||||||
|
String matchedName = matchedUserName(originalText, user);
|
||||||
|
if (StringUtils.isNotBlank(matchedName) && matchedName.length() > matchedLength)
|
||||||
|
{
|
||||||
|
matchedUser = user;
|
||||||
|
matchedLength = matchedName.length();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchedUser != null)
|
||||||
|
{
|
||||||
|
plan.setOperatorName(StringUtils.defaultIfBlank(matchedUser.getNickName(), matchedUser.getUserName()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(plan.getOperatorName()))
|
||||||
|
{
|
||||||
|
plan.setOperatorName(resolveNickName(username));
|
||||||
|
plan.getWarnings().add("未识别到人员,默认使用当前登录用户");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String matchedUserName(String text, SysUser user)
|
||||||
|
{
|
||||||
|
String nickName = user == null ? null : user.getNickName();
|
||||||
|
String userName = user == null ? null : user.getUserName();
|
||||||
|
if (StringUtils.isNotBlank(nickName) && text.contains(nickName))
|
||||||
|
{
|
||||||
|
return nickName;
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(userName) && text.toLowerCase(Locale.ROOT).contains(userName.toLowerCase(Locale.ROOT)))
|
||||||
|
{
|
||||||
|
return userName;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean noAreaCandidates(AgentPlan plan)
|
private boolean noAreaCandidates(AgentPlan plan)
|
||||||
{
|
{
|
||||||
for (AgentCandidate candidate : plan.getCandidates())
|
for (AgentCandidate candidate : plan.getCandidates())
|
||||||
@ -1150,12 +1334,23 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
|
|
||||||
private int productScore(AgentPlan plan, String originalText, ProductInfo product)
|
private int productScore(AgentPlan plan, String originalText, ProductInfo product)
|
||||||
{
|
{
|
||||||
|
String requestedPortSpec = extractPortSpec(originalText);
|
||||||
|
String productPortSpec = extractPortSpec(product.getSpec());
|
||||||
|
if (StringUtils.isNotBlank(requestedPortSpec) && StringUtils.isNotBlank(productPortSpec)
|
||||||
|
&& !requestedPortSpec.equals(productPortSpec))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
int score = 0;
|
int score = 0;
|
||||||
score += fieldScore(plan.getProductName(), product.getProductName(), 8);
|
score += fieldScore(plan.getProductName(), product.getProductName(), 8);
|
||||||
score += fieldScore(plan.getBrand(), product.getBrand(), 4);
|
score += fieldScore(plan.getBrand(), product.getBrand(), 4);
|
||||||
score += fieldScore(plan.getCategory(), product.getCategory(), 4);
|
score += fieldScore(plan.getCategory(), product.getCategory(), 4);
|
||||||
score += fieldScore(plan.getSpec(), product.getSpec(), 4);
|
score += fieldScore(plan.getSpec(), product.getSpec(), 14);
|
||||||
score += fieldScore(plan.getUnit(), product.getUnit(), 1);
|
score += fieldScore(plan.getUnit(), product.getUnit(), 1);
|
||||||
|
if (StringUtils.isNotBlank(requestedPortSpec) && requestedPortSpec.equals(productPortSpec))
|
||||||
|
{
|
||||||
|
score += 24;
|
||||||
|
}
|
||||||
String fullInfo = productFullText(product);
|
String fullInfo = productFullText(product);
|
||||||
if (containsIgnoreCase(originalText, fullInfo) || containsIgnoreCase(fullInfo, originalText))
|
if (containsIgnoreCase(originalText, fullInfo) || containsIgnoreCase(fullInfo, originalText))
|
||||||
{
|
{
|
||||||
@ -1166,7 +1361,7 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
score += containsField(originalText, product.getProductName()) ? 4 : 0;
|
score += containsField(originalText, product.getProductName()) ? 4 : 0;
|
||||||
score += containsField(originalText, product.getBrand()) ? 2 : 0;
|
score += containsField(originalText, product.getBrand()) ? 2 : 0;
|
||||||
score += containsField(originalText, product.getCategory()) ? 2 : 0;
|
score += containsField(originalText, product.getCategory()) ? 2 : 0;
|
||||||
score += containsField(originalText, product.getSpec()) ? 2 : 0;
|
score += containsNormalized(originalText, product.getSpec()) ? 10 : 0;
|
||||||
}
|
}
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
@ -1177,11 +1372,11 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (input.trim().equalsIgnoreCase(target.trim()))
|
if (normalizeSearchText(input).equals(normalizeSearchText(target)))
|
||||||
{
|
{
|
||||||
return exactScore;
|
return exactScore;
|
||||||
}
|
}
|
||||||
if (containsIgnoreCase(input, target) || containsIgnoreCase(target, input))
|
if (containsNormalized(input, target) || containsNormalized(target, input))
|
||||||
{
|
{
|
||||||
return Math.max(1, exactScore / 2);
|
return Math.max(1, exactScore / 2);
|
||||||
}
|
}
|
||||||
@ -1218,6 +1413,38 @@ public class AgentServiceImpl implements IAgentService
|
|||||||
return text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT));
|
return text.toLowerCase(Locale.ROOT).contains(keyword.toLowerCase(Locale.ROOT));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean containsNormalized(String text, String keyword)
|
||||||
|
{
|
||||||
|
String normalizedText = normalizeSearchText(text);
|
||||||
|
String normalizedKeyword = normalizeSearchText(keyword);
|
||||||
|
return StringUtils.isNotBlank(normalizedText) && StringUtils.isNotBlank(normalizedKeyword)
|
||||||
|
&& normalizedText.contains(normalizedKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeSearchText(String value)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(value))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Normalizer.normalize(value, Normalizer.Form.NFKC)
|
||||||
|
.toLowerCase(Locale.ROOT)
|
||||||
|
.replace('×', 'x')
|
||||||
|
.replace('*', '*')
|
||||||
|
.replaceAll("[\\s\\p{Punct},。;:、]+", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractPortSpec(String text)
|
||||||
|
{
|
||||||
|
String source = Normalizer.normalize(StringUtils.defaultString(text), Normalizer.Form.NFKC);
|
||||||
|
Matcher matcher = PORT_SPEC_PATTERN.matcher(source);
|
||||||
|
if (!matcher.find())
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return matcher.group(1) + "进" + matcher.group(2) + "出";
|
||||||
|
}
|
||||||
|
|
||||||
private void fillProduct(AgentPlan plan, ProductInfo product)
|
private void fillProduct(AgentPlan plan, ProductInfo product)
|
||||||
{
|
{
|
||||||
plan.setProductId(product.getId());
|
plan.setProductId(product.getId());
|
||||||
|
|||||||
@ -78,6 +78,16 @@ public class InRecordServiceImpl implements IInRecordService
|
|||||||
{
|
{
|
||||||
throw new ServiceException("已入库记录不能修改");
|
throw new ServiceException("已入库记录不能修改");
|
||||||
}
|
}
|
||||||
|
if (dbRecord != null && dbRecord.getArrivalId() != null)
|
||||||
|
{
|
||||||
|
inRecord.setInNo(dbRecord.getInNo());
|
||||||
|
inRecord.setProductId(dbRecord.getProductId());
|
||||||
|
inRecord.setPurchaseRequestId(dbRecord.getPurchaseRequestId());
|
||||||
|
inRecord.setArrivalId(dbRecord.getArrivalId());
|
||||||
|
inRecord.setSourceNo(dbRecord.getSourceNo());
|
||||||
|
inRecord.setInType(dbRecord.getInType());
|
||||||
|
inRecord.setInNum(dbRecord.getInNum());
|
||||||
|
}
|
||||||
return inRecordMapper.updateInRecord(inRecord);
|
return inRecordMapper.updateInRecord(inRecord);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +101,10 @@ public class InRecordServiceImpl implements IInRecordService
|
|||||||
{
|
{
|
||||||
throw new ServiceException("已入库记录不能删除");
|
throw new ServiceException("已入库记录不能删除");
|
||||||
}
|
}
|
||||||
|
if (dbRecord != null && dbRecord.getArrivalId() != null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购验收生成的待入库记录不能删除");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return inRecordMapper.deleteInRecordByIds(ids);
|
return inRecordMapper.deleteInRecordByIds(ids);
|
||||||
}
|
}
|
||||||
@ -108,6 +122,15 @@ public class InRecordServiceImpl implements IInRecordService
|
|||||||
{
|
{
|
||||||
throw new ServiceException("该入库记录已确认");
|
throw new ServiceException("该入库记录已确认");
|
||||||
}
|
}
|
||||||
|
validateInRecord(inRecord);
|
||||||
|
if (inRecord.getAreaId() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("请先选择库区再确认入库");
|
||||||
|
}
|
||||||
|
if (inRecord.getProductId() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("入库产品不能为空");
|
||||||
|
}
|
||||||
stockInfoMapper.upsertIncreaseStock(inRecord.getAreaId(), inRecord.getProductId(), inRecord.getInNum());
|
stockInfoMapper.upsertIncreaseStock(inRecord.getAreaId(), inRecord.getProductId(), inRecord.getInNum());
|
||||||
InRecord confirmRecord = new InRecord();
|
InRecord confirmRecord = new InRecord();
|
||||||
confirmRecord.setId(id);
|
confirmRecord.setId(id);
|
||||||
|
|||||||
@ -0,0 +1,274 @@
|
|||||||
|
package com.ruoyi.warehouse.service.impl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.warehouse.domain.InRecord;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrival;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrivalAcceptanceRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseArrivalRevokeRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseRequest;
|
||||||
|
import com.ruoyi.warehouse.mapper.InRecordMapper;
|
||||||
|
import com.ruoyi.warehouse.mapper.PurchaseArrivalMapper;
|
||||||
|
import com.ruoyi.warehouse.mapper.PurchaseRequestMapper;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseArrivalService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PurchaseArrivalServiceImpl implements IPurchaseArrivalService
|
||||||
|
{
|
||||||
|
private static final AtomicInteger ARRIVAL_SEQUENCE = new AtomicInteger();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PurchaseArrivalMapper purchaseArrivalMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PurchaseRequestMapper purchaseRequestMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private InRecordMapper inRecordMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PurchaseArrival> selectPurchaseArrivalList(Long purchaseRequestId)
|
||||||
|
{
|
||||||
|
if (purchaseRequestMapper.selectPurchaseRequestById(purchaseRequestId, null) == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购申请不存在");
|
||||||
|
}
|
||||||
|
return purchaseArrivalMapper.selectPurchaseArrivalList(purchaseRequestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int insertPurchaseArrival(PurchaseArrival arrival)
|
||||||
|
{
|
||||||
|
if (arrival == null || arrival.getPurchaseRequestId() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购申请不能为空");
|
||||||
|
}
|
||||||
|
PurchaseRequest request = purchaseRequestMapper.selectPurchaseRequestByIdForUpdate(arrival.getPurchaseRequestId());
|
||||||
|
requireOwnerOrManager(request);
|
||||||
|
if (Integer.valueOf(5).equals(request.getPurchaseStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("已取消的采购申请不能登记到货");
|
||||||
|
}
|
||||||
|
if (arrival.getArrivalQuantity() == null || arrival.getArrivalQuantity().signum() <= 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("到货数量必须大于0");
|
||||||
|
}
|
||||||
|
if (arrival.getArrivalDate() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("到货日期不能为空");
|
||||||
|
}
|
||||||
|
BigDecimal activeQuantity = purchaseArrivalMapper.selectActiveQuantityByRequestId(request.getId());
|
||||||
|
if (activeQuantity.add(arrival.getArrivalQuantity()).compareTo(request.getQuantity()) > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("本次到货数量超过剩余可登记数量");
|
||||||
|
}
|
||||||
|
arrival.setArrivalNo(generateArrivalNo());
|
||||||
|
arrival.setAcceptanceStatus(0);
|
||||||
|
arrival.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
arrival.setRemark(trimToNull(arrival.getRemark()));
|
||||||
|
return purchaseArrivalMapper.insertPurchaseArrival(arrival);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int acceptPurchaseArrival(Long id, PurchaseArrivalAcceptanceRequest acceptance)
|
||||||
|
{
|
||||||
|
PurchaseArrival arrival = purchaseArrivalMapper.selectPurchaseArrivalByIdForUpdate(id);
|
||||||
|
if (arrival == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("到货记录不存在");
|
||||||
|
}
|
||||||
|
requireOwnerOrManager(arrival.getApplicantId());
|
||||||
|
if (!Integer.valueOf(0).equals(arrival.getAcceptanceStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("该到货记录已验收,不能重复操作");
|
||||||
|
}
|
||||||
|
if (acceptance == null || acceptance.getAcceptanceStatus() == null
|
||||||
|
|| (acceptance.getAcceptanceStatus() != 1 && acceptance.getAcceptanceStatus() != 2))
|
||||||
|
{
|
||||||
|
throw new ServiceException("验收结果不正确");
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
PurchaseRequest request = null;
|
||||||
|
if (Integer.valueOf(1).equals(acceptance.getAcceptanceStatus()))
|
||||||
|
{
|
||||||
|
request = purchaseRequestMapper.selectPurchaseRequestByIdForUpdate(arrival.getPurchaseRequestId());
|
||||||
|
arrival.setPreAcceptPurchaseStatus(request.getPurchaseStatus());
|
||||||
|
}
|
||||||
|
arrival.setAcceptanceStatus(acceptance.getAcceptanceStatus());
|
||||||
|
arrival.setAcceptanceRemark(trimToNull(acceptance.getAcceptanceRemark()));
|
||||||
|
arrival.setAcceptedBy(username);
|
||||||
|
arrival.setAcceptedTime(new Date());
|
||||||
|
arrival.setUpdateBy(username);
|
||||||
|
int rows = purchaseArrivalMapper.updatePurchaseArrivalAcceptance(arrival);
|
||||||
|
|
||||||
|
if (Integer.valueOf(1).equals(arrival.getAcceptanceStatus()))
|
||||||
|
{
|
||||||
|
createPendingInRecord(arrival, username);
|
||||||
|
BigDecimal accepted = purchaseArrivalMapper.selectAcceptedQuantityByRequestId(arrival.getPurchaseRequestId());
|
||||||
|
int status = accepted.compareTo(request.getQuantity()) >= 0 ? 4 : 3;
|
||||||
|
purchaseRequestMapper.updatePurchaseStatus(request.getId(), status,
|
||||||
|
arrival.getAcceptanceRemark(), username);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int revokePurchaseArrivalAcceptance(Long id, PurchaseArrivalRevokeRequest revoke)
|
||||||
|
{
|
||||||
|
PurchaseArrival arrival = purchaseArrivalMapper.selectPurchaseArrivalByIdForUpdate(id);
|
||||||
|
if (arrival == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("到货记录不存在");
|
||||||
|
}
|
||||||
|
requireOwnerOrManager(arrival.getApplicantId());
|
||||||
|
if (Integer.valueOf(0).equals(arrival.getAcceptanceStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("该到货记录尚未验收,无需撤销");
|
||||||
|
}
|
||||||
|
if (revoke == null || StringUtils.isBlank(revoke.getRevokeRemark()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("撤销原因不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
PurchaseRequest request = purchaseRequestMapper.selectPurchaseRequestByIdForUpdate(arrival.getPurchaseRequestId());
|
||||||
|
if (Integer.valueOf(2).equals(arrival.getAcceptanceStatus()))
|
||||||
|
{
|
||||||
|
if (Integer.valueOf(5).equals(request.getPurchaseStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购申请已取消,不能撤销拒收");
|
||||||
|
}
|
||||||
|
BigDecimal activeQuantity = purchaseArrivalMapper.selectActiveQuantityByRequestId(request.getId());
|
||||||
|
if (activeQuantity.add(arrival.getArrivalQuantity()).compareTo(request.getQuantity()) > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("拒收后已登记替代到货,撤销会超过采购数量,请先删除对应的待验收到货");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InRecord inRecord = inRecordMapper.selectInRecordByArrivalIdForUpdate(id);
|
||||||
|
if (inRecord == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("未找到验收生成的待入库记录,不能自动撤销");
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(1).equals(inRecord.getInStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("该批到货已经确认入库,不能直接撤销,请先办理库存冲销");
|
||||||
|
}
|
||||||
|
if (inRecordMapper.deletePendingInRecordByArrivalId(id) != 1)
|
||||||
|
{
|
||||||
|
throw new ServiceException("待入库记录状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
arrival.setRevokedBy(username);
|
||||||
|
arrival.setRevokedTime(new Date());
|
||||||
|
arrival.setRevokeRemark(revoke.getRevokeRemark().trim());
|
||||||
|
arrival.setUpdateBy(username);
|
||||||
|
int rows = purchaseArrivalMapper.revokePurchaseArrivalAcceptance(arrival);
|
||||||
|
if (rows != 1)
|
||||||
|
{
|
||||||
|
throw new ServiceException("到货验收状态已变化,请刷新后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Integer.valueOf(1).equals(arrival.getAcceptanceStatus()))
|
||||||
|
{
|
||||||
|
BigDecimal accepted = purchaseArrivalMapper.selectAcceptedQuantityByRequestId(request.getId());
|
||||||
|
int status;
|
||||||
|
if (accepted.signum() == 0)
|
||||||
|
{
|
||||||
|
Integer previousStatus = arrival.getPreAcceptPurchaseStatus();
|
||||||
|
status = previousStatus != null && previousStatus >= 0 && previousStatus <= 2 ? previousStatus : 2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
status = accepted.compareTo(request.getQuantity()) >= 0 ? 4 : 3;
|
||||||
|
}
|
||||||
|
purchaseRequestMapper.updatePurchaseStatus(request.getId(), status,
|
||||||
|
"撤销到货验收:" + arrival.getRevokeRemark(), username);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int deletePurchaseArrival(Long id)
|
||||||
|
{
|
||||||
|
PurchaseArrival arrival = purchaseArrivalMapper.selectPurchaseArrivalById(id);
|
||||||
|
if (arrival == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("到货记录不存在");
|
||||||
|
}
|
||||||
|
requireOwnerOrManager(arrival.getApplicantId());
|
||||||
|
if (!Integer.valueOf(0).equals(arrival.getAcceptanceStatus()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("已验收的到货记录不能删除");
|
||||||
|
}
|
||||||
|
return purchaseArrivalMapper.deletePendingPurchaseArrival(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createPendingInRecord(PurchaseArrival arrival, String username)
|
||||||
|
{
|
||||||
|
InRecord inRecord = new InRecord();
|
||||||
|
inRecord.setInNo("AIN" + arrival.getId());
|
||||||
|
inRecord.setAreaId(null);
|
||||||
|
inRecord.setProductId(arrival.getProductId());
|
||||||
|
inRecord.setPurchaseRequestId(arrival.getPurchaseRequestId());
|
||||||
|
inRecord.setArrivalId(arrival.getId());
|
||||||
|
inRecord.setSourceNo(arrival.getArrivalNo());
|
||||||
|
inRecord.setInType(2);
|
||||||
|
inRecord.setInNum(arrival.getArrivalQuantity());
|
||||||
|
inRecord.setInStatus(0);
|
||||||
|
inRecord.setOperator(username);
|
||||||
|
inRecord.setInOperator(null);
|
||||||
|
inRecord.setRemark("采购到货验收:" + arrival.getRequestNo());
|
||||||
|
inRecordMapper.insertInRecord(inRecord);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireOwnerOrManager(PurchaseRequest request)
|
||||||
|
{
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购申请不存在");
|
||||||
|
}
|
||||||
|
requireOwnerOrManager(request.getApplicantId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireOwnerOrManager(Long applicantId)
|
||||||
|
{
|
||||||
|
if (!isManager() && !SecurityUtils.getUserId().equals(applicantId))
|
||||||
|
{
|
||||||
|
throw new ServiceException("只能处理自己的采购到货");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isManager()
|
||||||
|
{
|
||||||
|
return SecurityUtils.isAdmin() || SecurityUtils.hasPermi("warehouse:purchase:manage");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value)
|
||||||
|
{
|
||||||
|
return StringUtils.isBlank(value) ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateArrivalNo()
|
||||||
|
{
|
||||||
|
int sequence = ARRIVAL_SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1);
|
||||||
|
return "PA" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())
|
||||||
|
+ String.format("%03d", sequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,141 @@
|
|||||||
|
package com.ruoyi.warehouse.service.impl;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseBatch;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseBatchAssignRequest;
|
||||||
|
import com.ruoyi.warehouse.mapper.PurchaseBatchMapper;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseBatchService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PurchaseBatchServiceImpl implements IPurchaseBatchService
|
||||||
|
{
|
||||||
|
@Autowired
|
||||||
|
private PurchaseBatchMapper purchaseBatchMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PurchaseBatch> selectPurchaseBatchList(PurchaseBatch batch)
|
||||||
|
{
|
||||||
|
return purchaseBatchMapper.selectPurchaseBatchList(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PurchaseBatch> selectPurchaseBatchOptions(String keyword)
|
||||||
|
{
|
||||||
|
return purchaseBatchMapper.selectPurchaseBatchOptions(StringUtils.trim(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PurchaseBatch> selectPurchaseBatchFilterOptions(String keyword)
|
||||||
|
{
|
||||||
|
return purchaseBatchMapper.selectPurchaseBatchFilterOptions(StringUtils.trim(keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int insertPurchaseBatch(PurchaseBatch batch)
|
||||||
|
{
|
||||||
|
checkManager();
|
||||||
|
if (batch.getPurchaseStatus() == null)
|
||||||
|
{
|
||||||
|
batch.setPurchaseStatus(1);
|
||||||
|
}
|
||||||
|
normalizeAndValidate(batch);
|
||||||
|
if (purchaseBatchMapper.selectPurchaseBatchByNo(batch.getBatchNo()) != null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购批次已存在");
|
||||||
|
}
|
||||||
|
batch.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
return purchaseBatchMapper.insertPurchaseBatch(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int updatePurchaseBatch(PurchaseBatch batch)
|
||||||
|
{
|
||||||
|
checkManager();
|
||||||
|
if (batch.getId() == null || purchaseBatchMapper.selectPurchaseBatchById(batch.getId()) == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购批次不存在");
|
||||||
|
}
|
||||||
|
normalizeAndValidate(batch);
|
||||||
|
PurchaseBatch sameNo = purchaseBatchMapper.selectPurchaseBatchByNo(batch.getBatchNo());
|
||||||
|
if (sameNo != null && !sameNo.getId().equals(batch.getId()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购批次已存在");
|
||||||
|
}
|
||||||
|
String username = SecurityUtils.getUsername();
|
||||||
|
batch.setUpdateBy(username);
|
||||||
|
int rows = purchaseBatchMapper.updatePurchaseBatch(batch);
|
||||||
|
purchaseBatchMapper.syncRequestBatchNo(batch.getId(), batch.getBatchNo(), username);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int assignPurchaseBatch(PurchaseBatchAssignRequest request)
|
||||||
|
{
|
||||||
|
checkManager();
|
||||||
|
if (request == null || request.getRequestIds() == null || request.getRequestIds().length == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("请选择要分配或延期的采购申请");
|
||||||
|
}
|
||||||
|
if (request.getBatchId() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("请选择目标采购批次");
|
||||||
|
}
|
||||||
|
PurchaseBatch batch = purchaseBatchMapper.selectPurchaseBatchById(request.getBatchId());
|
||||||
|
if (batch == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("目标采购批次不存在");
|
||||||
|
}
|
||||||
|
if (purchaseBatchMapper.countRequestsByIds(request.getRequestIds()) != request.getRequestIds().length)
|
||||||
|
{
|
||||||
|
throw new ServiceException("部分采购申请不存在,批次分配失败");
|
||||||
|
}
|
||||||
|
purchaseBatchMapper.assignRequests(request.getRequestIds(), batch, SecurityUtils.getUsername());
|
||||||
|
return request.getRequestIds().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int deletePurchaseBatchById(Long id)
|
||||||
|
{
|
||||||
|
checkManager();
|
||||||
|
if (purchaseBatchMapper.selectPurchaseBatchById(id) == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购批次不存在");
|
||||||
|
}
|
||||||
|
if (purchaseBatchMapper.countRequestsByBatchId(id) > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("该批次仍有关联的采购申请,不能删除");
|
||||||
|
}
|
||||||
|
return purchaseBatchMapper.deletePurchaseBatchById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalizeAndValidate(PurchaseBatch batch)
|
||||||
|
{
|
||||||
|
batch.setBatchNo(StringUtils.trim(batch.getBatchNo()));
|
||||||
|
batch.setBatchName(StringUtils.trim(batch.getBatchName()));
|
||||||
|
batch.setProgressRemark(StringUtils.trim(batch.getProgressRemark()));
|
||||||
|
if (StringUtils.isBlank(batch.getBatchNo()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购批次编号不能为空");
|
||||||
|
}
|
||||||
|
if (batch.getPurchaseStatus() == null || batch.getPurchaseStatus() < 1 || batch.getPurchaseStatus() > 5)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购批次状态不正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkManager()
|
||||||
|
{
|
||||||
|
if (!SecurityUtils.isAdmin() && !SecurityUtils.hasPermi("warehouse:purchase:manage"))
|
||||||
|
{
|
||||||
|
throw new ServiceException("只有管理员可以维护采购批次");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,411 @@
|
|||||||
|
package com.ruoyi.warehouse.service.impl;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.SecurityUtils;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.warehouse.domain.ProductInfo;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseRequest;
|
||||||
|
import com.ruoyi.warehouse.domain.PurchaseStatusUpdateRequest;
|
||||||
|
import com.ruoyi.warehouse.mapper.ProductInfoMapper;
|
||||||
|
import com.ruoyi.warehouse.mapper.PurchaseArrivalMapper;
|
||||||
|
import com.ruoyi.warehouse.mapper.PurchaseRequestMapper;
|
||||||
|
import com.ruoyi.warehouse.service.IProductInfoService;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseRequestService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PurchaseRequestServiceImpl implements IPurchaseRequestService
|
||||||
|
{
|
||||||
|
private static final AtomicInteger REQUEST_SEQUENCE = new AtomicInteger();
|
||||||
|
@Autowired
|
||||||
|
private PurchaseRequestMapper purchaseRequestMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private ProductInfoMapper productInfoMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IProductInfoService productInfoService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private PurchaseArrivalMapper purchaseArrivalMapper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<PurchaseRequest> selectPurchaseRequestList(PurchaseRequest purchaseRequest)
|
||||||
|
{
|
||||||
|
return purchaseRequestMapper.selectPurchaseRequestList(purchaseRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PurchaseRequest selectPurchaseRequestById(Long id)
|
||||||
|
{
|
||||||
|
return requireRequest(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int insertPurchaseRequest(PurchaseRequest purchaseRequest)
|
||||||
|
{
|
||||||
|
normalize(purchaseRequest);
|
||||||
|
validateRequest(purchaseRequest);
|
||||||
|
purchaseRequest.setApplicantId(SecurityUtils.getUserId());
|
||||||
|
purchaseRequest.setApplicantName(SecurityUtils.getUsername());
|
||||||
|
purchaseRequest.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
purchaseRequest.setPurchaseStatus(0);
|
||||||
|
purchaseRequest.setPurchaseBatchNo(null);
|
||||||
|
purchaseRequest.setProgressRemark(null);
|
||||||
|
if (StringUtils.isBlank(purchaseRequest.getRequestNo()))
|
||||||
|
{
|
||||||
|
purchaseRequest.setRequestNo(generateRequestNo());
|
||||||
|
}
|
||||||
|
else if (purchaseRequestMapper.checkRequestNoUnique(purchaseRequest.getRequestNo()) != null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("申请单号已存在");
|
||||||
|
}
|
||||||
|
resolveProduct(purchaseRequest);
|
||||||
|
return purchaseRequestMapper.insertPurchaseRequest(purchaseRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int updatePurchaseRequest(PurchaseRequest purchaseRequest)
|
||||||
|
{
|
||||||
|
if (purchaseRequest.getId() == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购申请ID不能为空");
|
||||||
|
}
|
||||||
|
PurchaseRequest existing = requireRequest(purchaseRequest.getId());
|
||||||
|
checkOwnerOrManager(existing);
|
||||||
|
normalize(purchaseRequest);
|
||||||
|
validateRequest(purchaseRequest);
|
||||||
|
|
||||||
|
BigDecimal activeArrivalQuantity = purchaseArrivalMapper.selectActiveQuantityByRequestId(existing.getId());
|
||||||
|
if (activeArrivalQuantity.compareTo(purchaseRequest.getQuantity()) > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("需求数量不能小于已登记或已验收的到货数量");
|
||||||
|
}
|
||||||
|
if (activeArrivalQuantity.signum() > 0 && !sameMaterial(existing, purchaseRequest))
|
||||||
|
{
|
||||||
|
throw new ServiceException("已有到货记录,不能更换采购物料");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isBlank(purchaseRequest.getRequestNo()))
|
||||||
|
{
|
||||||
|
purchaseRequest.setRequestNo(existing.getRequestNo());
|
||||||
|
}
|
||||||
|
|
||||||
|
PurchaseRequest sameNo = purchaseRequestMapper.checkRequestNoUnique(purchaseRequest.getRequestNo());
|
||||||
|
if (sameNo != null && !sameNo.getId().equals(purchaseRequest.getId()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("申请单号已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 申请归属和采购进度只能由系统及进度管理接口维护。
|
||||||
|
purchaseRequest.setApplicantId(existing.getApplicantId());
|
||||||
|
purchaseRequest.setApplicantName(existing.getApplicantName());
|
||||||
|
purchaseRequest.setPurchaseStatus(existing.getPurchaseStatus());
|
||||||
|
purchaseRequest.setPurchaseBatchNo(existing.getPurchaseBatchNo());
|
||||||
|
purchaseRequest.setProgressRemark(existing.getProgressRemark());
|
||||||
|
purchaseRequest.setUpdateBy(SecurityUtils.getUsername());
|
||||||
|
resolveProduct(purchaseRequest);
|
||||||
|
int rows = purchaseRequestMapper.updatePurchaseRequest(purchaseRequest, currentApplicantScope());
|
||||||
|
if (rows == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("无权修改其他员工的采购申请");
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int deletePurchaseRequestByIds(Long[] ids)
|
||||||
|
{
|
||||||
|
if (ids == null || ids.length == 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("请选择采购申请");
|
||||||
|
}
|
||||||
|
for (Long id : ids)
|
||||||
|
{
|
||||||
|
PurchaseRequest request = requireRequest(id);
|
||||||
|
checkOwnerOrManager(request);
|
||||||
|
if (purchaseArrivalMapper.countByRequestId(id) > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("申请 " + request.getRequestNo() + " 已有到货记录,不能删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int rows = purchaseRequestMapper.deletePurchaseRequestByIds(ids, currentApplicantScope());
|
||||||
|
if (rows != ids.length)
|
||||||
|
{
|
||||||
|
throw new ServiceException("只能删除自己的采购申请");
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int updatePurchaseStatus(Long id, PurchaseStatusUpdateRequest request)
|
||||||
|
{
|
||||||
|
PurchaseRequest existing = requireRequest(id);
|
||||||
|
checkOwnerOrManager(existing);
|
||||||
|
Integer status = request.getPurchaseStatus();
|
||||||
|
if (status == null || (status != 0 && status != 1 && status != 2 && status != 5))
|
||||||
|
{
|
||||||
|
throw new ServiceException("到货相关状态由验收数量自动计算");
|
||||||
|
}
|
||||||
|
if (purchaseArrivalMapper.selectAcceptedQuantityByRequestId(id).signum() > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("已有验收到货,采购状态由验收进度自动计算");
|
||||||
|
}
|
||||||
|
if (status == 5 && purchaseArrivalMapper.selectActiveQuantityByRequestId(id).signum() > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("已有待验收或已验收到货,不能取消采购申请");
|
||||||
|
}
|
||||||
|
return purchaseRequestMapper.updatePurchaseStatus(id, status,
|
||||||
|
trimToNull(request.getProgressRemark()), SecurityUtils.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public int batchInsertPurchaseRequest(List<PurchaseRequest> requestList)
|
||||||
|
{
|
||||||
|
if (requestList == null || requestList.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("表格数据不能为空");
|
||||||
|
}
|
||||||
|
int count = 0;
|
||||||
|
for (int index = 0; index < requestList.size(); index++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
insertPurchaseRequest(requestList.get(index));
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new ServiceException("第" + (index + 1) + "行:" + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public String importPurchaseRequest(List<PurchaseRequest> requestList, String username)
|
||||||
|
{
|
||||||
|
if (requestList == null || requestList.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("导入数据不能为空");
|
||||||
|
}
|
||||||
|
int successCount = 0;
|
||||||
|
StringBuilder failure = new StringBuilder();
|
||||||
|
for (int index = 0; index < requestList.size(); index++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PurchaseRequest request = requestList.get(index);
|
||||||
|
request.setApplicantId(SecurityUtils.getUserId());
|
||||||
|
request.setApplicantName(username);
|
||||||
|
request.setCreateBy(username);
|
||||||
|
insertPurchaseRequest(request);
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
failure.append("第").append(index + 2).append("行:").append(e.getMessage()).append(";");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failure.length() > 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("导入失败," + failure);
|
||||||
|
}
|
||||||
|
return "成功导入 " + successCount + " 条采购申请";
|
||||||
|
}
|
||||||
|
|
||||||
|
private PurchaseRequest requireRequest(Long id)
|
||||||
|
{
|
||||||
|
PurchaseRequest request = purchaseRequestMapper.selectPurchaseRequestById(id, null);
|
||||||
|
if (request == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购申请不存在");
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void checkOwnerOrManager(PurchaseRequest request)
|
||||||
|
{
|
||||||
|
if (!isPurchaseManager() && !SecurityUtils.getUserId().equals(request.getApplicantId()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("无权操作其他员工的采购申请");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isPurchaseManager()
|
||||||
|
{
|
||||||
|
return SecurityUtils.isAdmin() || SecurityUtils.hasPermi("warehouse:purchase:manage");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long currentApplicantScope()
|
||||||
|
{
|
||||||
|
return isPurchaseManager() ? null : SecurityUtils.getUserId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void normalize(PurchaseRequest request)
|
||||||
|
{
|
||||||
|
request.setRequestNo(StringUtils.trim(request.getRequestNo()));
|
||||||
|
request.setProductCode(trimToNull(request.getProductCode()));
|
||||||
|
request.setMaterialName(StringUtils.trim(request.getMaterialName()));
|
||||||
|
request.setMaterialBrand(trimToNull(request.getMaterialBrand()));
|
||||||
|
request.setCategory(StringUtils.trim(request.getCategory()));
|
||||||
|
request.setMaterialParams(StringUtils.trim(request.getMaterialParams()));
|
||||||
|
request.setPurchaseLink(StringUtils.trim(request.getPurchaseLink()));
|
||||||
|
request.setUnit(StringUtils.trim(request.getUnit()));
|
||||||
|
request.setRequestRemark(trimToNull(request.getRequestRemark()));
|
||||||
|
if (StringUtils.isBlank(request.getRemark()) && StringUtils.isNotBlank(request.getRequestRemark()))
|
||||||
|
{
|
||||||
|
request.setRemark(request.getRequestRemark());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateRequest(PurchaseRequest request)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(request.getMaterialName()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("物料名称不能为空");
|
||||||
|
}
|
||||||
|
if (request.getQuantity() == null || request.getQuantity().signum() <= 0)
|
||||||
|
{
|
||||||
|
throw new ServiceException("需求数量必须大于0");
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(request.getPurchaseLink())
|
||||||
|
&& !request.getPurchaseLink().matches("(?i)^https?://.+"))
|
||||||
|
{
|
||||||
|
throw new ServiceException("采购链接必须以 http:// 或 https:// 开头");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resolveProduct(PurchaseRequest request)
|
||||||
|
{
|
||||||
|
ProductInfo draft = new ProductInfo();
|
||||||
|
draft.setProductCode(request.getProductCode());
|
||||||
|
draft.setProductName(request.getMaterialName());
|
||||||
|
draft.setBrand(request.getMaterialBrand());
|
||||||
|
draft.setCategory(trimToNull(request.getCategory()));
|
||||||
|
draft.setSpec(trimToNull(request.getMaterialParams()));
|
||||||
|
draft.setUnit(trimToNull(request.getUnit()));
|
||||||
|
draft.setRemark(trimToNull(request.getProductRemark()));
|
||||||
|
|
||||||
|
ProductInfo product = null;
|
||||||
|
if (request.getProductId() != null)
|
||||||
|
{
|
||||||
|
ProductInfo selected = productInfoMapper.selectProductInfoById(request.getProductId());
|
||||||
|
if (selected == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("选择的产品信息不存在");
|
||||||
|
}
|
||||||
|
if (sameProduct(draft, selected))
|
||||||
|
{
|
||||||
|
product = selected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
product = resolveProductByCode(draft);
|
||||||
|
}
|
||||||
|
if (product == null && StringUtils.isBlank(draft.getCategory()))
|
||||||
|
{
|
||||||
|
draft.setCategory(productInfoService.inferProductCategory(draft));
|
||||||
|
request.setCategory(draft.getCategory());
|
||||||
|
}
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
product = productInfoMapper.checkProductInfoUnique(draft);
|
||||||
|
}
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
productInfoService.insertProductInfo(draft);
|
||||||
|
product = draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
request.setProductId(product.getId());
|
||||||
|
request.setProductCode(product.getProductCode());
|
||||||
|
request.setMaterialName(product.getProductName());
|
||||||
|
request.setMaterialBrand(product.getBrand());
|
||||||
|
request.setCategory(product.getCategory());
|
||||||
|
request.setMaterialParams(product.getSpec());
|
||||||
|
request.setUnit(product.getUnit());
|
||||||
|
request.setProductRemark(product.getRemark());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProductInfo resolveProductByCode(ProductInfo draft)
|
||||||
|
{
|
||||||
|
if (StringUtils.isBlank(draft.getProductCode()))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
ProductInfo product = productInfoMapper.checkProductCodeUnique(draft.getProductCode());
|
||||||
|
if (product == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!matchesProvidedField(draft.getProductName(), product.getProductName())
|
||||||
|
|| !matchesProvidedField(draft.getBrand(), product.getBrand())
|
||||||
|
|| !matchesProvidedField(draft.getCategory(), product.getCategory())
|
||||||
|
|| !matchesProvidedField(draft.getSpec(), product.getSpec())
|
||||||
|
|| !matchesProvidedField(draft.getUnit(), product.getUnit()))
|
||||||
|
{
|
||||||
|
throw new ServiceException("物料编码 " + draft.getProductCode() + " 与导入的物料信息不一致");
|
||||||
|
}
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean matchesProvidedField(String imported, String existing)
|
||||||
|
{
|
||||||
|
return StringUtils.isBlank(imported) || sameText(imported, existing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameProduct(ProductInfo left, ProductInfo right)
|
||||||
|
{
|
||||||
|
return sameText(left.getProductName(), right.getProductName())
|
||||||
|
&& sameText(left.getBrand(), right.getBrand())
|
||||||
|
&& sameText(left.getCategory(), right.getCategory())
|
||||||
|
&& sameText(left.getSpec(), right.getSpec());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameMaterial(PurchaseRequest left, PurchaseRequest right)
|
||||||
|
{
|
||||||
|
return sameText(left.getMaterialName(), right.getMaterialName())
|
||||||
|
&& sameText(left.getMaterialBrand(), right.getMaterialBrand())
|
||||||
|
&& sameText(left.getCategory(), right.getCategory())
|
||||||
|
&& sameText(left.getMaterialParams(), right.getMaterialParams());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sameText(String left, String right)
|
||||||
|
{
|
||||||
|
String leftValue = trimToNull(left);
|
||||||
|
String rightValue = trimToNull(right);
|
||||||
|
if (leftValue == null || rightValue == null)
|
||||||
|
{
|
||||||
|
return leftValue == rightValue;
|
||||||
|
}
|
||||||
|
return leftValue.equalsIgnoreCase(rightValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value)
|
||||||
|
{
|
||||||
|
return StringUtils.isBlank(value) ? null : value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateRequestNo()
|
||||||
|
{
|
||||||
|
int sequence = REQUEST_SEQUENCE.updateAndGet(value -> value >= 999 ? 0 : value + 1);
|
||||||
|
return "PR" + new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date())
|
||||||
|
+ String.format("%03d", sequence);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,756 @@
|
|||||||
|
package com.ruoyi.warehouse.service.impl;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.http.HttpClient;
|
||||||
|
import java.net.http.HttpRequest;
|
||||||
|
import java.net.http.HttpResponse;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.CompletionException;
|
||||||
|
import org.apache.commons.io.FilenameUtils;
|
||||||
|
import org.apache.poi.ss.usermodel.DataFormatter;
|
||||||
|
import org.apache.poi.ss.usermodel.FormulaEvaluator;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||||
|
import org.apache.poi.ss.util.CellRangeAddress;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.JSONArray;
|
||||||
|
import com.alibaba.fastjson2.JSONObject;
|
||||||
|
import com.ruoyi.common.exception.ServiceException;
|
||||||
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
|
import com.ruoyi.warehouse.domain.ProductInfo;
|
||||||
|
import com.ruoyi.warehouse.service.IProductInfoService;
|
||||||
|
import com.ruoyi.warehouse.service.IPurchaseSmartImportService;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PurchaseSmartImportServiceImpl implements IPurchaseSmartImportService
|
||||||
|
{
|
||||||
|
private static final int MAX_ROWS = 100;
|
||||||
|
private static final int REVIEW_CHUNK_SIZE = 8;
|
||||||
|
private static final long MAX_FILE_SIZE = 10L * 1024 * 1024;
|
||||||
|
private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder()
|
||||||
|
.connectTimeout(Duration.ofSeconds(10))
|
||||||
|
.version(HttpClient.Version.HTTP_1_1)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
@Value("${agent.model.base-url}")
|
||||||
|
private String textModelBaseUrl;
|
||||||
|
|
||||||
|
@Value("${agent.model.api-key:}")
|
||||||
|
private String textModelApiKey;
|
||||||
|
|
||||||
|
@Value("${agent.model.model}")
|
||||||
|
private String textModel;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private IProductInfoService productService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONObject analyze(MultipartFile file) throws Exception
|
||||||
|
{
|
||||||
|
validateFile(file);
|
||||||
|
String extension = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(Locale.ROOT);
|
||||||
|
List<ProductInfo> products = productService.selectProductInfoAll();
|
||||||
|
String output = isExcel(extension) ? analyzeExcel(file, products) : analyzeImage(file, extension, products);
|
||||||
|
JSONObject result = normalizeResult(output, products);
|
||||||
|
if (result.getJSONArray("items").isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("没有识别到可导入的采购明细");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String analyzeExcel(MultipartFile file, List<ProductInfo> products) throws Exception
|
||||||
|
{
|
||||||
|
JSONArray rows = readExcel(file);
|
||||||
|
if (rows.isEmpty())
|
||||||
|
{
|
||||||
|
throw new ServiceException("Excel中没有可分析的数据");
|
||||||
|
}
|
||||||
|
JSONObject mapping = resolveHeaderMapping(rows);
|
||||||
|
JSONArray items = new JSONArray();
|
||||||
|
for (int i = 0; i < rows.size(); i++)
|
||||||
|
{
|
||||||
|
JSONObject source = rows.getJSONObject(i);
|
||||||
|
JSONObject item = new JSONObject();
|
||||||
|
for (String header : mapping.keySet())
|
||||||
|
{
|
||||||
|
String field = mapping.getString(header);
|
||||||
|
Object value = source.get(header);
|
||||||
|
if (StringUtils.isNotBlank(field) && value != null && StringUtils.isNotBlank(value.toString()))
|
||||||
|
{
|
||||||
|
item.put(field, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!item.isEmpty()) items.add(item);
|
||||||
|
}
|
||||||
|
reviewExcelItems(items, products);
|
||||||
|
propagateBrands(items, products);
|
||||||
|
return JSONObject.of("items", items).toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void propagateBrands(JSONArray items, List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
List<String> brands = new ArrayList<>();
|
||||||
|
for (ProductInfo product : products) addDistinctBrand(brands, product.getBrand());
|
||||||
|
for (int i = 0; i < items.size(); i++) addDistinctBrand(brands, items.getJSONObject(i).getString("materialBrand"));
|
||||||
|
brands.sort(Comparator.comparingInt(String::length).reversed());
|
||||||
|
|
||||||
|
for (int i = 0; i < items.size(); i++)
|
||||||
|
{
|
||||||
|
JSONObject item = items.getJSONObject(i);
|
||||||
|
if (StringUtils.isNotBlank(item.getString("materialBrand"))) continue;
|
||||||
|
String spec = normalizeSearchText(item.getString("materialParams"));
|
||||||
|
if (StringUtils.isBlank(spec)) continue;
|
||||||
|
for (String brand : brands)
|
||||||
|
{
|
||||||
|
if (!spec.startsWith(normalizeSearchText(brand))) continue;
|
||||||
|
item.put("materialBrand", brand);
|
||||||
|
JSONArray inferred = item.getJSONArray("inferredFields");
|
||||||
|
if (inferred == null)
|
||||||
|
{
|
||||||
|
inferred = new JSONArray();
|
||||||
|
item.put("inferredFields", inferred);
|
||||||
|
}
|
||||||
|
if (!inferred.contains("materialBrand")) inferred.add("materialBrand");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDistinctBrand(List<String> brands, String brand)
|
||||||
|
{
|
||||||
|
String value = StringUtils.trim(brand);
|
||||||
|
if (StringUtils.isBlank(value)) return;
|
||||||
|
for (String existing : brands)
|
||||||
|
{
|
||||||
|
if (same(existing, value)) return;
|
||||||
|
}
|
||||||
|
brands.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reviewExcelItems(JSONArray items, List<ProductInfo> products) throws Exception
|
||||||
|
{
|
||||||
|
JSONArray existingCategories = existingCategories(products);
|
||||||
|
List<CompletableFuture<String>> tasks = new ArrayList<>();
|
||||||
|
for (int start = 0; start < items.size(); start += REVIEW_CHUNK_SIZE)
|
||||||
|
{
|
||||||
|
JSONArray rows = new JSONArray();
|
||||||
|
for (int i = start; i < Math.min(start + REVIEW_CHUNK_SIZE, items.size()); i++)
|
||||||
|
{
|
||||||
|
JSONObject row = new JSONObject(items.getJSONObject(i));
|
||||||
|
row.put("_rowIndex", i);
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
String prompt = "逐行检查采购数据内容。识别单元格内容错位、名称和型号混在一起、数量带单位、日期格式不规范等问题,"
|
||||||
|
+ "并补全品牌、类别、单位。每一行的category都不能为空,优先从existingCategories选择最合适分类;"
|
||||||
|
+ "无法精确判断时选择最接近的通用分类。不要改变正确内容,不要臆造品牌或型号。"
|
||||||
|
+ "quantity只能来自原数量列;规格中的包装数量(如【600个】、每包500个、1000PCS)属于materialParams,"
|
||||||
|
+ "绝不能作为采购quantity。原quantity是有效数字时禁止修改,原数量缺失时也不要从规格推断。"
|
||||||
|
+ "使用紧凑JSON且不要格式化换行,只返回需要修改或补全的字段,格式:"
|
||||||
|
+ "{\"patches\":[{\"rowIndex\":0,\"fields\":{\"字段\":\"新值\"},\"warning\":\"仅有歧义时说明\"}]}。"
|
||||||
|
+ "常规补全不要返回warning或inferredFields。允许字段仅为productCode,materialName,materialBrand,"
|
||||||
|
+ "category,materialParams,unit,quantity,expectedDate,purchaseLink,productRemark,remark。无修改返回空patches。rows="
|
||||||
|
+ rows.toJSONString() + ",existingCategories=" + existingCategories.toJSONString();
|
||||||
|
tasks.add(CompletableFuture.supplyAsync(() -> {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return callInventoryModel(JSONArray.of(
|
||||||
|
message("system", "你是采购Excel数据校验器,只返回JSON对象。"), message("user", prompt)), 2048);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new CompletionException(e);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (CompletableFuture<String> task : tasks)
|
||||||
|
{
|
||||||
|
JSONObject result = JSON.parseObject(extractJson(task.join()));
|
||||||
|
JSONArray patches = result.getJSONArray("patches");
|
||||||
|
if (patches == null) continue;
|
||||||
|
for (int i = 0; i < patches.size(); i++) applyReviewPatch(items, patches.getJSONObject(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (CompletionException e)
|
||||||
|
{
|
||||||
|
Throwable cause = e.getCause();
|
||||||
|
if (cause instanceof ServiceException) throw (ServiceException) cause;
|
||||||
|
throw new ServiceException("库存AI校验超时或失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyReviewPatch(JSONArray items, JSONObject patch)
|
||||||
|
{
|
||||||
|
if (patch == null) return;
|
||||||
|
Integer rowIndex = patch.getInteger("rowIndex");
|
||||||
|
if (rowIndex == null || rowIndex < 0 || rowIndex >= items.size()) return;
|
||||||
|
JSONObject item = items.getJSONObject(rowIndex);
|
||||||
|
JSONObject fields = patch.getJSONObject("fields");
|
||||||
|
if (fields != null)
|
||||||
|
{
|
||||||
|
JSONArray inferred = new JSONArray();
|
||||||
|
for (String field : fields.keySet())
|
||||||
|
{
|
||||||
|
if (isImportField(field))
|
||||||
|
{
|
||||||
|
if ("quantity".equals(field) && !canApplyQuantityPatch(item.get("quantity"))) continue;
|
||||||
|
item.put(field, fields.get(field));
|
||||||
|
inferred.add(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!inferred.isEmpty()) item.put("inferredFields", inferred);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(patch.getString("warning"))) item.put("warning", patch.getString("warning"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canApplyQuantityPatch(Object originalQuantity)
|
||||||
|
{
|
||||||
|
if (originalQuantity == null || StringUtils.isBlank(originalQuantity.toString())) return false;
|
||||||
|
String value = originalQuantity.toString().replace(",", "").trim();
|
||||||
|
return !value.matches("^\\d+(?:\\.\\d+)?$");
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONArray existingCategories(List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
JSONArray categories = new JSONArray();
|
||||||
|
for (ProductInfo product : products)
|
||||||
|
{
|
||||||
|
String category = StringUtils.trim(product.getCategory());
|
||||||
|
if (StringUtils.isNotBlank(category) && !categories.contains(category)) categories.add(category);
|
||||||
|
if (categories.size() >= 100) break;
|
||||||
|
}
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject resolveHeaderMapping(JSONArray rows) throws Exception
|
||||||
|
{
|
||||||
|
JSONObject mapping = new JSONObject();
|
||||||
|
JSONObject first = rows.getJSONObject(0);
|
||||||
|
JSONArray unknownHeaders = new JSONArray();
|
||||||
|
for (String header : first.keySet())
|
||||||
|
{
|
||||||
|
if ("_sourceRow".equals(header)) continue;
|
||||||
|
String field = knownHeaderField(header);
|
||||||
|
if (field == null)
|
||||||
|
{
|
||||||
|
if (!isIgnoredHeader(header)) unknownHeaders.add(header);
|
||||||
|
}
|
||||||
|
else mapping.put(header, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mapping.containsValue("materialName") || !unknownHeaders.isEmpty())
|
||||||
|
{
|
||||||
|
JSONArray samples = new JSONArray();
|
||||||
|
for (int i = 0; i < Math.min(3, rows.size()); i++) samples.add(rows.get(i));
|
||||||
|
String prompt = "只分析Excel表头映射,不处理每行数据。可用目标字段为productCode,materialName,"
|
||||||
|
+ "materialBrand,category,materialParams,unit,quantity,expectedDate,purchaseLink,productRemark,remark。"
|
||||||
|
+ "单价、总价、申请人等无关列不要映射。只返回JSON:{\"mapping\":{\"原表头\":\"目标字段\"}}。"
|
||||||
|
+ "headers=" + first.keySet() + ", samples=" + samples.toJSONString();
|
||||||
|
String output = callInventoryModel(JSONArray.of(message("system", "你是Excel表头映射器,只返回JSON对象。"),
|
||||||
|
message("user", prompt)), 512);
|
||||||
|
JSONObject aiMapping = JSON.parseObject(extractJson(output)).getJSONObject("mapping");
|
||||||
|
if (aiMapping != null)
|
||||||
|
{
|
||||||
|
for (String header : aiMapping.keySet())
|
||||||
|
{
|
||||||
|
String field = aiMapping.getString(header);
|
||||||
|
if (first.containsKey(header) && isImportField(field)) mapping.put(header, field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!mapping.containsValue("materialName"))
|
||||||
|
{
|
||||||
|
throw new ServiceException("AI未能识别物料名称列,请检查Excel表头");
|
||||||
|
}
|
||||||
|
return mapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String analyzeImage(MultipartFile file, String extension, List<ProductInfo> products) throws Exception
|
||||||
|
{
|
||||||
|
String mime = "jpg".equals(extension) || "jpeg".equals(extension) ? "image/jpeg" : "image/" + extension;
|
||||||
|
JSONArray content = new JSONArray();
|
||||||
|
content.add(JSONObject.of("type", "text", "text",
|
||||||
|
"识别图片中的采购表格,并参考 existingProducts 关联物料或补全缺失信息。existingProducts="
|
||||||
|
+ productCatalog(products).toJSONString()));
|
||||||
|
content.add(JSONObject.of("type", "image_url", "image_url", JSONObject.of("url",
|
||||||
|
"data:" + mime + ";base64," + Base64.getEncoder().encodeToString(file.getBytes()))));
|
||||||
|
return callInventoryModel(JSONArray.of(message("system", systemPrompt()), message("user", content)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONArray readExcel(MultipartFile file) throws Exception
|
||||||
|
{
|
||||||
|
JSONArray result = new JSONArray();
|
||||||
|
try (InputStream input = file.getInputStream(); Workbook workbook = WorkbookFactory.create(input))
|
||||||
|
{
|
||||||
|
DataFormatter formatter = new DataFormatter(Locale.CHINA);
|
||||||
|
FormulaEvaluator evaluator = workbook.getCreationHelper().createFormulaEvaluator();
|
||||||
|
HeaderLocation location = findHeader(workbook, formatter, evaluator);
|
||||||
|
if (location == null)
|
||||||
|
{
|
||||||
|
throw new ServiceException("未识别到Excel表头,请确认表头包含物料名称、品名或产品名称");
|
||||||
|
}
|
||||||
|
Sheet sheet = location.sheet;
|
||||||
|
int headerIndex = location.rowIndex;
|
||||||
|
Row header = sheet.getRow(headerIndex);
|
||||||
|
int lastColumn = lastHeaderColumn(sheet, headerIndex, header);
|
||||||
|
for (int rowIndex = headerIndex + 1; rowIndex <= sheet.getLastRowNum() && result.size() < MAX_ROWS; rowIndex++)
|
||||||
|
{
|
||||||
|
Row row = sheet.getRow(rowIndex);
|
||||||
|
JSONObject values = new JSONObject();
|
||||||
|
if (row != null)
|
||||||
|
{
|
||||||
|
for (int column = 0; column < lastColumn; column++)
|
||||||
|
{
|
||||||
|
String name = headerCellText(sheet, headerIndex, column, formatter, evaluator);
|
||||||
|
String value = dataCellText(sheet, rowIndex, column, formatter, evaluator);
|
||||||
|
if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(value))
|
||||||
|
{
|
||||||
|
values.put(name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!values.isEmpty())
|
||||||
|
{
|
||||||
|
values.put("_sourceRow", rowIndex + 1);
|
||||||
|
result.add(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HeaderLocation findHeader(Workbook workbook, DataFormatter formatter, FormulaEvaluator evaluator)
|
||||||
|
{
|
||||||
|
HeaderLocation best = null;
|
||||||
|
int bestScore = 0;
|
||||||
|
HeaderLocation fallback = null;
|
||||||
|
int fallbackScore = 0;
|
||||||
|
for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++)
|
||||||
|
{
|
||||||
|
Sheet sheet = workbook.getSheetAt(sheetIndex);
|
||||||
|
int end = Math.min(sheet.getLastRowNum(), sheet.getFirstRowNum() + 50);
|
||||||
|
for (int i = sheet.getFirstRowNum(); i <= end; i++)
|
||||||
|
{
|
||||||
|
Row row = sheet.getRow(i);
|
||||||
|
int nonEmpty = 0;
|
||||||
|
int recognized = 0;
|
||||||
|
boolean hasNameHeader = false;
|
||||||
|
int rowLastColumn = lastHeaderColumn(sheet, i, row);
|
||||||
|
if (rowLastColumn > 0)
|
||||||
|
{
|
||||||
|
for (int column = 0; column < rowLastColumn; column++)
|
||||||
|
{
|
||||||
|
String value = headerCellText(sheet, i, column, formatter, evaluator).replaceAll("\\s+", "")
|
||||||
|
.toLowerCase(Locale.ROOT);
|
||||||
|
if (StringUtils.isBlank(value)) continue;
|
||||||
|
nonEmpty++;
|
||||||
|
hasNameHeader |= value.contains("物料名称") || value.contains("产品名称")
|
||||||
|
|| value.contains("物品名称") || value.equals("物品") || value.equals("物料")
|
||||||
|
|| value.equals("产品") || value.equals("品名") || value.equals("名称")
|
||||||
|
|| value.equals("materialname") || value.equals("productname") || value.equals("itemname");
|
||||||
|
if (isKnownHeader(value)) recognized++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int score = recognized * 10 + nonEmpty;
|
||||||
|
if ((hasNameHeader || recognized >= 3) && nonEmpty >= 2
|
||||||
|
&& (score > bestScore || score == bestScore && best != null && i > best.rowIndex))
|
||||||
|
{
|
||||||
|
best = new HeaderLocation(sheet, i);
|
||||||
|
bestScore = score;
|
||||||
|
}
|
||||||
|
// AI负责最终字段映射。这里保留一个宽松候选,避免陌生表头在调用AI前被拦截。
|
||||||
|
if (recognized >= 2 && nonEmpty >= 3 && score > fallbackScore)
|
||||||
|
{
|
||||||
|
fallback = new HeaderLocation(sheet, i);
|
||||||
|
fallbackScore = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best != null ? best : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isKnownHeader(String value)
|
||||||
|
{
|
||||||
|
return value.contains("物料") || value.contains("物品") || value.contains("产品")
|
||||||
|
|| value.contains("品名") || value.equals("名称")
|
||||||
|
|| value.contains("规格") || value.contains("型号") || value.contains("参数")
|
||||||
|
|| value.contains("数量") || value.contains("品牌") || value.contains("类别")
|
||||||
|
|| value.contains("分类") || value.contains("单位") || value.contains("链接")
|
||||||
|
|| value.contains("日期") || value.contains("备注") || value.contains("material")
|
||||||
|
|| value.contains("product") || value.contains("quantity") || value.contains("spec");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HeaderLocation
|
||||||
|
{
|
||||||
|
private final Sheet sheet;
|
||||||
|
private final int rowIndex;
|
||||||
|
|
||||||
|
private HeaderLocation(Sheet sheet, int rowIndex)
|
||||||
|
{
|
||||||
|
this.sheet = sheet;
|
||||||
|
this.rowIndex = rowIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String cellText(Row row, int column, DataFormatter formatter, FormulaEvaluator evaluator)
|
||||||
|
{
|
||||||
|
if (row == null || row.getCell(column) == null)
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return StringUtils.trim(formatter.formatCellValue(row.getCell(column), evaluator));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String headerCellText(Sheet sheet, int rowIndex, int column, DataFormatter formatter,
|
||||||
|
FormulaEvaluator evaluator)
|
||||||
|
{
|
||||||
|
return mergedCellText(sheet, rowIndex, column, formatter, evaluator, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String dataCellText(Sheet sheet, int rowIndex, int column, DataFormatter formatter,
|
||||||
|
FormulaEvaluator evaluator)
|
||||||
|
{
|
||||||
|
return mergedCellText(sheet, rowIndex, column, formatter, evaluator, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String mergedCellText(Sheet sheet, int rowIndex, int column, DataFormatter formatter,
|
||||||
|
FormulaEvaluator evaluator, boolean resolveHorizontal)
|
||||||
|
{
|
||||||
|
Row row = sheet.getRow(rowIndex);
|
||||||
|
String direct = cellText(row, column, formatter, evaluator);
|
||||||
|
if (StringUtils.isNotBlank(direct)) return direct;
|
||||||
|
for (int i = 0; i < sheet.getNumMergedRegions(); i++)
|
||||||
|
{
|
||||||
|
CellRangeAddress region = sheet.getMergedRegion(i);
|
||||||
|
if (!region.isInRange(rowIndex, column)) continue;
|
||||||
|
if (!resolveHorizontal && region.getFirstColumn() != region.getLastColumn()) return "";
|
||||||
|
return cellText(sheet.getRow(region.getFirstRow()), region.getFirstColumn(), formatter, evaluator);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
private int lastHeaderColumn(Sheet sheet, int headerIndex, Row header)
|
||||||
|
{
|
||||||
|
int lastColumn = header == null ? 0 : Math.max(0, header.getLastCellNum());
|
||||||
|
for (int i = 0; i < sheet.getNumMergedRegions(); i++)
|
||||||
|
{
|
||||||
|
CellRangeAddress region = sheet.getMergedRegion(i);
|
||||||
|
if (headerIndex >= region.getFirstRow() && headerIndex <= region.getLastRow())
|
||||||
|
{
|
||||||
|
lastColumn = Math.max(lastColumn, region.getLastColumn() + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastColumn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONArray productCatalog(List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
JSONArray catalog = new JSONArray();
|
||||||
|
for (ProductInfo product : products)
|
||||||
|
{
|
||||||
|
if (catalog.size() >= 300)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catalog.add(JSONObject.of("productId", product.getId(), "productCode", product.getProductCode(),
|
||||||
|
"materialName", product.getProductName(), "materialBrand", product.getBrand(),
|
||||||
|
"category", product.getCategory(), "materialParams", product.getSpec(), "unit", product.getUnit()));
|
||||||
|
}
|
||||||
|
return catalog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String knownHeaderField(String header)
|
||||||
|
{
|
||||||
|
String value = normalizeSearchText(header);
|
||||||
|
if (List.of("关联物料编码", "物料编码", "产品编码", "productcode", "materialcode").contains(value)) return "productCode";
|
||||||
|
if (List.of("物料名称", "产品名称", "物品名称", "商品名称", "物品", "物料", "产品", "品名", "名称",
|
||||||
|
"materialname", "productname", "itemname").contains(value)) return "materialName";
|
||||||
|
if (List.of("品牌", "厂牌", "brand").contains(value)) return "materialBrand";
|
||||||
|
if (List.of("类别", "分类", "物料类别", "产品分类", "category").contains(value)) return "category";
|
||||||
|
if (List.of("参数规格", "规格型号", "型号规格", "规格", "型号", "参数", "spec", "model").contains(value)) return "materialParams";
|
||||||
|
if (List.of("单位", "计量单位", "unit").contains(value)) return "unit";
|
||||||
|
if (List.of("需求数量", "采购数量", "数量", "qty", "quantity").contains(value)) return "quantity";
|
||||||
|
if (List.of("期望到货日期", "期望到货", "预计到货日期", "到货日期", "expecteddate").contains(value)) return "expectedDate";
|
||||||
|
if (List.of("采购链接", "参考链接", "商品链接", "链接", "url", "purchaselink").contains(value)) return "purchaseLink";
|
||||||
|
if (List.of("产品备注", "物料备注", "productremark").contains(value)) return "productRemark";
|
||||||
|
if (List.of("申请备注", "备注", "remark").contains(value)) return "remark";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isImportField(String field)
|
||||||
|
{
|
||||||
|
return List.of("productCode", "materialName", "materialBrand", "category", "materialParams", "unit",
|
||||||
|
"quantity", "expectedDate", "purchaseLink", "productRemark", "remark").contains(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isIgnoredHeader(String header)
|
||||||
|
{
|
||||||
|
String value = normalizeSearchText(header);
|
||||||
|
return List.of("单价", "价格", "含税单价", "总价", "金额", "合计", "申请人", "申请者", "申请员工",
|
||||||
|
"序号", "行号", "编号", "部门", "税率", "供应商").contains(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeSearchText(String value)
|
||||||
|
{
|
||||||
|
return StringUtils.defaultString(value).replaceAll("[\\s_()()/\\-]+", "").toLowerCase(Locale.ROOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callInventoryModel(JSONArray input, int maxTokens) throws Exception
|
||||||
|
{
|
||||||
|
return callInventoryModel(input, maxTokens, Duration.ofSeconds(120));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callInventoryModel(JSONArray input) throws Exception
|
||||||
|
{
|
||||||
|
return callInventoryModel(input, 8192, Duration.ofSeconds(120));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String callInventoryModel(JSONArray input, int maxTokens, Duration timeout) throws Exception
|
||||||
|
{
|
||||||
|
JSONObject body = new JSONObject();
|
||||||
|
body.put("model", textModel);
|
||||||
|
body.put("messages", input);
|
||||||
|
body.put("chat_template_kwargs", JSONObject.of("enable_thinking", false));
|
||||||
|
body.put("temperature", 0);
|
||||||
|
body.put("max_tokens", maxTokens);
|
||||||
|
body.put("stream", false);
|
||||||
|
HttpRequest.Builder builder = HttpRequest.newBuilder().uri(URI.create(normalizeChatEndpoint(textModelBaseUrl)))
|
||||||
|
.timeout(timeout).header("Content-Type", "application/json")
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8));
|
||||||
|
if (StringUtils.isNotBlank(textModelApiKey))
|
||||||
|
{
|
||||||
|
builder.header("Authorization", "Bearer " + textModelApiKey);
|
||||||
|
}
|
||||||
|
HttpResponse<String> response = HTTP_CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
|
if (response.statusCode() < 200 || response.statusCode() >= 300)
|
||||||
|
{
|
||||||
|
throw new ServiceException("库存AI分析失败,HTTP " + response.statusCode());
|
||||||
|
}
|
||||||
|
JSONObject root = JSON.parseObject(response.body());
|
||||||
|
JSONArray choices = root.getJSONArray("choices");
|
||||||
|
String text = choices == null || choices.isEmpty() ? null : choices.getJSONObject(0).getJSONObject("message").getString("content");
|
||||||
|
if (StringUtils.isBlank(text))
|
||||||
|
{
|
||||||
|
throw new ServiceException("库存AI未返回分析内容");
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject normalizeResult(String output, List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
JSONObject root = JSON.parseObject(extractJson(output));
|
||||||
|
JSONArray sourceItems = root.getJSONArray("items");
|
||||||
|
JSONArray items = new JSONArray();
|
||||||
|
List<String> globalWarnings = new ArrayList<>();
|
||||||
|
if (sourceItems == null)
|
||||||
|
{
|
||||||
|
return JSONObject.of("items", items, "warnings", JSONArray.of("模型返回格式不正确"));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < sourceItems.size() && items.size() < MAX_ROWS; i++)
|
||||||
|
{
|
||||||
|
JSONObject source = sourceItems.getJSONObject(i);
|
||||||
|
if (source == null || StringUtils.isBlank(source.getString("materialName")))
|
||||||
|
{
|
||||||
|
globalWarnings.add("第 " + (i + 1) + " 行未识别到物料名称,已跳过");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
JSONObject item = new JSONObject();
|
||||||
|
copyFields(source, item);
|
||||||
|
ProductInfo matched = matchProduct(item, products);
|
||||||
|
if (matched != null)
|
||||||
|
{
|
||||||
|
item.put("productId", matched.getId());
|
||||||
|
item.put("productCode", matched.getProductCode());
|
||||||
|
item.put("materialName", matched.getProductName());
|
||||||
|
item.put("materialBrand", matched.getBrand());
|
||||||
|
item.put("category", matched.getCategory());
|
||||||
|
item.put("materialParams", matched.getSpec());
|
||||||
|
item.put("unit", matched.getUnit());
|
||||||
|
String materialLabel = StringUtils.isNotBlank(matched.getProductCode())
|
||||||
|
? matched.getProductCode() : matched.getProductName() + " / " + matched.getSpec();
|
||||||
|
appendTip(item, "已关联现有物料 " + materialLabel);
|
||||||
|
}
|
||||||
|
if (source.getJSONArray("inferredFields") != null && !source.getJSONArray("inferredFields").isEmpty())
|
||||||
|
{
|
||||||
|
List<String> labels = new ArrayList<>();
|
||||||
|
for (String field : source.getJSONArray("inferredFields").toJavaList(String.class))
|
||||||
|
{
|
||||||
|
String label = importFieldLabel(field);
|
||||||
|
if (!labels.contains(label)) labels.add(label);
|
||||||
|
}
|
||||||
|
appendTip(item, "AI推断字段:" + String.join("、", labels));
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(source.getString("warning")))
|
||||||
|
{
|
||||||
|
appendTip(item, source.getString("warning"));
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(item.getString("category")))
|
||||||
|
{
|
||||||
|
String category = categoryByMaterialName(item.getString("materialName"), products);
|
||||||
|
if (StringUtils.isBlank(category)) category = defaultCategory(products);
|
||||||
|
item.put("category", category);
|
||||||
|
appendTip(item, "未能精确判断分类,已归入" + category);
|
||||||
|
}
|
||||||
|
items.add(item);
|
||||||
|
}
|
||||||
|
return JSONObject.of("items", items, "warnings", globalWarnings);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String categoryByMaterialName(String materialName, List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
String category = null;
|
||||||
|
for (ProductInfo product : products)
|
||||||
|
{
|
||||||
|
if (!same(materialName, product.getProductName()) || StringUtils.isBlank(product.getCategory())) continue;
|
||||||
|
if (category != null && !same(category, product.getCategory())) return null;
|
||||||
|
category = product.getCategory();
|
||||||
|
}
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String defaultCategory(List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
String[] preferred = { "其他", "通用物料", "其他物料", "未分类" };
|
||||||
|
for (String candidate : preferred)
|
||||||
|
{
|
||||||
|
for (ProductInfo product : products)
|
||||||
|
{
|
||||||
|
if (same(candidate, product.getCategory())) return product.getCategory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "其他";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void copyFields(JSONObject source, JSONObject target)
|
||||||
|
{
|
||||||
|
String[] fields = { "productCode", "materialName", "materialBrand", "category",
|
||||||
|
"materialParams", "unit", "quantity", "expectedDate", "purchaseLink", "productRemark", "remark" };
|
||||||
|
for (String field : fields)
|
||||||
|
{
|
||||||
|
target.put(field, source.get(field));
|
||||||
|
}
|
||||||
|
if (target.getBigDecimal("quantity") == null || target.getBigDecimal("quantity").signum() <= 0)
|
||||||
|
{
|
||||||
|
target.put("quantity", BigDecimal.ONE);
|
||||||
|
appendTip(target, "未识别到有效数量,已暂填1");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String importFieldLabel(String field)
|
||||||
|
{
|
||||||
|
if ("productCode".equals(field)) return "关联物料编码";
|
||||||
|
if ("materialName".equals(field)) return "物料名称";
|
||||||
|
if ("materialBrand".equals(field)) return "品牌";
|
||||||
|
if ("category".equals(field)) return "分类";
|
||||||
|
if ("materialParams".equals(field)) return "参数规格";
|
||||||
|
if ("unit".equals(field)) return "单位";
|
||||||
|
if ("quantity".equals(field)) return "数量";
|
||||||
|
if ("expectedDate".equals(field)) return "期望到货日期";
|
||||||
|
if ("purchaseLink".equals(field)) return "采购链接";
|
||||||
|
if ("productRemark".equals(field)) return "产品备注";
|
||||||
|
if ("remark".equals(field)) return "申请备注";
|
||||||
|
return field;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProductInfo matchProduct(JSONObject item, List<ProductInfo> products)
|
||||||
|
{
|
||||||
|
String code = item.getString("productCode");
|
||||||
|
if (StringUtils.isNotBlank(code))
|
||||||
|
{
|
||||||
|
for (ProductInfo product : products)
|
||||||
|
{
|
||||||
|
if (same(code, product.getProductCode()))
|
||||||
|
{
|
||||||
|
return product;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<ProductInfo> matches = new ArrayList<>();
|
||||||
|
for (ProductInfo product : products)
|
||||||
|
{
|
||||||
|
if (same(item.getString("materialName"), product.getProductName())
|
||||||
|
&& (StringUtils.isBlank(item.getString("materialParams")) || same(item.getString("materialParams"), product.getSpec())))
|
||||||
|
{
|
||||||
|
matches.add(product);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return matches.size() == 1 ? matches.get(0) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendTip(JSONObject item, String tip)
|
||||||
|
{
|
||||||
|
String current = item.getString("smartTip");
|
||||||
|
item.put("smartTip", StringUtils.isBlank(current) ? tip : current + ";" + tip);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean same(String left, String right)
|
||||||
|
{
|
||||||
|
return StringUtils.isNotBlank(left) && StringUtils.isNotBlank(right) && left.trim().equalsIgnoreCase(right.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private JSONObject message(String role, Object content)
|
||||||
|
{
|
||||||
|
return JSONObject.of("role", role, "content", content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String systemPrompt()
|
||||||
|
{
|
||||||
|
return "你是采购清单智能整理器,只返回JSON对象。输出格式为{items:[...]},每项字段必须包含"
|
||||||
|
+ "productId,productCode,materialName,materialBrand,category,materialParams,unit,quantity,expectedDate,purchaseLink,productRemark,remark,inferredFields,warning。"
|
||||||
|
+ "识别任意中文或英文表头并正确映射,不能因列顺序不同而错位。单价、总价和申请人不是导入字段,不要混入其他字段。"
|
||||||
|
+ "参数规格中若含品牌或明确型号,可补全materialBrand;能从知名型号可靠判断品牌时允许推断,否则品牌留空。"
|
||||||
|
+ "category可合理归类,quantity缺失填null,expectedDate统一yyyy-MM-dd。"
|
||||||
|
+ "优先从existingProducts中选择完全一致的物料并返回其productId和productCode,不要把相似但型号不同的物料关联。"
|
||||||
|
+ "inferredFields列出所有不是源表直接提供、而是系统匹配或AI推断得到的字段名;不确定信息写入warning。最多返回100项。";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeChatEndpoint(String configuredUrl)
|
||||||
|
{
|
||||||
|
String url = StringUtils.defaultString(configuredUrl).trim();
|
||||||
|
while (url.endsWith("/")) url = url.substring(0, url.length() - 1);
|
||||||
|
if (url.endsWith("/v1")) return url + "/chat/completions";
|
||||||
|
if (url.endsWith("/v1/chat/completions") || url.endsWith("/chat/completions")) return url;
|
||||||
|
return url + "/v1/chat/completions";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String extractJson(String output)
|
||||||
|
{
|
||||||
|
int start = output.indexOf('{');
|
||||||
|
int end = output.lastIndexOf('}');
|
||||||
|
return start >= 0 && end > start ? output.substring(start, end + 1) : output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isExcel(String extension)
|
||||||
|
{
|
||||||
|
return "xls".equals(extension) || "xlsx".equals(extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateFile(MultipartFile file)
|
||||||
|
{
|
||||||
|
if (file == null || file.isEmpty()) throw new ServiceException("请选择图片或Excel文件");
|
||||||
|
if (file.getSize() > MAX_FILE_SIZE) throw new ServiceException("文件不能超过10MB");
|
||||||
|
String extension = FilenameUtils.getExtension(file.getOriginalFilename()).toLowerCase(Locale.ROOT);
|
||||||
|
if (!List.of("xls", "xlsx", "png", "jpg", "jpeg", "webp").contains(extension))
|
||||||
|
{
|
||||||
|
throw new ServiceException("仅支持 xls、xlsx、png、jpg、jpeg、webp 文件");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -79,13 +79,13 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService
|
|||||||
private static final int ITEM_DONE = 3;
|
private static final int ITEM_DONE = 3;
|
||||||
private static final int ITEM_FAILED = 4;
|
private static final int ITEM_FAILED = 4;
|
||||||
|
|
||||||
@Value("${agent.openai.base-url}")
|
@Value("${agent.model.base-url}")
|
||||||
private String baseUrl;
|
private String baseUrl;
|
||||||
|
|
||||||
@Value("${agent.openai.api-key}")
|
@Value("${agent.model.api-key:}")
|
||||||
private String apiKey;
|
private String apiKey;
|
||||||
|
|
||||||
@Value("${agent.openai.model}")
|
@Value("${agent.model.model}")
|
||||||
private String model;
|
private String model;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
@ -381,10 +381,6 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService
|
|||||||
|
|
||||||
private List<WmsBatchItem> parseImage(WmsBatchJob job, String username) throws Exception
|
private List<WmsBatchItem> parseImage(WmsBatchJob job, String username) throws Exception
|
||||||
{
|
{
|
||||||
if (StringUtils.isBlank(apiKey))
|
|
||||||
{
|
|
||||||
throw new ServiceException("图片导入需要配置 OPENAI_API_KEY 环境变量");
|
|
||||||
}
|
|
||||||
updateJob(job.getId(), JOB_PARSING, 30, null);
|
updateJob(job.getId(), JOB_PARSING, 30, null);
|
||||||
String output = callVisionModel(job);
|
String output = callVisionModel(job);
|
||||||
updateJob(job.getId(), JOB_PARSING, 70, null);
|
updateJob(job.getId(), JOB_PARSING, 70, null);
|
||||||
@ -560,36 +556,34 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService
|
|||||||
|
|
||||||
JSONObject body = new JSONObject();
|
JSONObject body = new JSONObject();
|
||||||
body.put("model", model);
|
body.put("model", model);
|
||||||
JSONArray input = new JSONArray();
|
JSONArray messages = new JSONArray();
|
||||||
input.add(message("system", visionSystemPrompt()));
|
messages.add(message("system", visionSystemPrompt()));
|
||||||
|
|
||||||
JSONArray content = new JSONArray();
|
JSONArray content = new JSONArray();
|
||||||
JSONObject text = new JSONObject();
|
JSONObject text = new JSONObject();
|
||||||
text.put("type", "input_text");
|
text.put("type", "text");
|
||||||
text.put("text", visionUserPrompt());
|
text.put("text", visionUserPrompt());
|
||||||
content.add(text);
|
content.add(text);
|
||||||
JSONObject imageContent = new JSONObject();
|
JSONObject imageContent = new JSONObject();
|
||||||
imageContent.put("type", "input_image");
|
imageContent.put("type", "image_url");
|
||||||
imageContent.put("image_url", "data:" + mimeType + ";base64," + encoded);
|
imageContent.put("image_url", JSONObject.of("url", "data:" + mimeType + ";base64," + encoded));
|
||||||
content.add(imageContent);
|
content.add(imageContent);
|
||||||
input.add(message("user", content));
|
messages.add(message("user", content));
|
||||||
body.put("input", input);
|
body.put("messages", messages);
|
||||||
|
body.put("chat_template_kwargs", JSONObject.of("enable_thinking", false));
|
||||||
|
body.put("temperature", 0);
|
||||||
|
body.put("max_tokens", 8192);
|
||||||
|
body.put("stream", false);
|
||||||
|
|
||||||
JSONObject format = new JSONObject();
|
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10))
|
||||||
format.put("type", "json_object");
|
.version(HttpClient.Version.HTTP_1_1).build();
|
||||||
JSONObject textFormat = new JSONObject();
|
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
|
||||||
textFormat.put("format", format);
|
|
||||||
body.put("text", textFormat);
|
|
||||||
|
|
||||||
HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build();
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
|
||||||
.uri(URI.create(normalizeEndpoint(baseUrl)))
|
.uri(URI.create(normalizeEndpoint(baseUrl)))
|
||||||
.timeout(Duration.ofSeconds(60))
|
.timeout(Duration.ofSeconds(120))
|
||||||
.header("Authorization", "Bearer " + apiKey)
|
|
||||||
.header("Content-Type", "application/json")
|
.header("Content-Type", "application/json")
|
||||||
.POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8))
|
.POST(HttpRequest.BodyPublishers.ofString(body.toJSONString(), StandardCharsets.UTF_8));
|
||||||
.build();
|
if (StringUtils.isNotBlank(apiKey)) requestBuilder.header("Authorization", "Bearer " + apiKey);
|
||||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
HttpResponse<String> response = client.send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
|
||||||
if (response.statusCode() < 200 || response.statusCode() >= 300)
|
if (response.statusCode() < 200 || response.statusCode() >= 300)
|
||||||
{
|
{
|
||||||
throw new ServiceException("图片模型解析失败,HTTP " + response.statusCode());
|
throw new ServiceException("图片模型解析失败,HTTP " + response.statusCode());
|
||||||
@ -1207,25 +1201,34 @@ public class WmsBatchImportServiceImpl implements IWmsBatchImportService
|
|||||||
|
|
||||||
private String normalizeEndpoint(String configuredBaseUrl)
|
private String normalizeEndpoint(String configuredBaseUrl)
|
||||||
{
|
{
|
||||||
String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:18080").trim();
|
String url = StringUtils.defaultIfBlank(configuredBaseUrl, "http://192.168.28.10:14081").trim();
|
||||||
while (url.endsWith("/"))
|
while (url.endsWith("/"))
|
||||||
{
|
{
|
||||||
url = url.substring(0, url.length() - 1);
|
url = url.substring(0, url.length() - 1);
|
||||||
}
|
}
|
||||||
if (url.endsWith("/v1"))
|
if (url.endsWith("/v1"))
|
||||||
{
|
{
|
||||||
return url + "/responses";
|
return url + "/chat/completions";
|
||||||
}
|
}
|
||||||
if (url.endsWith("/v1/responses"))
|
if (url.endsWith("/v1/chat/completions") || url.endsWith("/chat/completions"))
|
||||||
{
|
{
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
return url + "/v1/responses";
|
return url + "/v1/chat/completions";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String extractOutputText(String responseBody)
|
private String extractOutputText(String responseBody)
|
||||||
{
|
{
|
||||||
JSONObject root = JSON.parseObject(responseBody);
|
JSONObject root = JSON.parseObject(responseBody);
|
||||||
|
JSONArray choices = root.getJSONArray("choices");
|
||||||
|
if (choices != null && !choices.isEmpty())
|
||||||
|
{
|
||||||
|
JSONObject message = choices.getJSONObject(0).getJSONObject("message");
|
||||||
|
if (message != null && StringUtils.isNotBlank(message.getString("content")))
|
||||||
|
{
|
||||||
|
return message.getString("content");
|
||||||
|
}
|
||||||
|
}
|
||||||
String outputText = root.getString("output_text");
|
String outputText = root.getString("output_text");
|
||||||
if (StringUtils.isNotBlank(outputText))
|
if (StringUtils.isNotBlank(outputText))
|
||||||
{
|
{
|
||||||
|
|||||||
@ -6,6 +6,9 @@
|
|||||||
<result property="inNo" column="in_no"/>
|
<result property="inNo" column="in_no"/>
|
||||||
<result property="areaId" column="area_id"/>
|
<result property="areaId" column="area_id"/>
|
||||||
<result property="productId" column="product_id"/>
|
<result property="productId" column="product_id"/>
|
||||||
|
<result property="purchaseRequestId" column="purchase_request_id"/>
|
||||||
|
<result property="arrivalId" column="arrival_id"/>
|
||||||
|
<result property="sourceNo" column="source_no"/>
|
||||||
<result property="inType" column="in_type"/>
|
<result property="inType" column="in_type"/>
|
||||||
<result property="inNum" column="in_num"/>
|
<result property="inNum" column="in_num"/>
|
||||||
<result property="inStatus" column="in_status"/>
|
<result property="inStatus" column="in_status"/>
|
||||||
@ -26,7 +29,8 @@
|
|||||||
</resultMap>
|
</resultMap>
|
||||||
|
|
||||||
<sql id="selectInRecordVo">
|
<sql id="selectInRecordVo">
|
||||||
select ir.id, ir.in_no, ir.area_id, ir.product_id, ir.in_type, ir.in_num, ir.in_status,
|
select ir.id, ir.in_no, ir.area_id, ir.product_id, ir.purchase_request_id, ir.arrival_id, ir.source_no,
|
||||||
|
ir.in_type, ir.in_num, ir.in_status,
|
||||||
ir.operator, ir.in_operator, ir.confirm_time, ir.remark, ir.create_time, ir.update_time,
|
ir.operator, ir.in_operator, ir.confirm_time, ir.remark, ir.create_time, ir.update_time,
|
||||||
wa.area_code, wa.area_name, pi.product_code, pi.product_name, pi.brand, pi.category, pi.spec, pi.unit
|
wa.area_code, wa.area_name, pi.product_code, pi.product_name, pi.brand, pi.category, pi.spec, pi.unit
|
||||||
from in_record ir
|
from in_record ir
|
||||||
@ -57,17 +61,26 @@
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectInRecordByIdForUpdate" parameterType="Long" resultMap="InRecordResult">
|
<select id="selectInRecordByIdForUpdate" parameterType="Long" resultMap="InRecordResult">
|
||||||
select id, in_no, area_id, product_id, in_type, in_num, in_status, operator, in_operator, confirm_time, remark, create_time, update_time
|
select id, in_no, area_id, product_id, purchase_request_id, arrival_id, source_no, in_type, in_num,
|
||||||
|
in_status, operator, in_operator, confirm_time, remark, create_time, update_time
|
||||||
from in_record where id = #{id} for update
|
from in_record where id = #{id} for update
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectInRecordByArrivalIdForUpdate" parameterType="Long" resultMap="InRecordResult">
|
||||||
|
select id, in_no, area_id, product_id, purchase_request_id, arrival_id, source_no, in_type, in_num,
|
||||||
|
in_status, operator, in_operator, confirm_time, remark, create_time, update_time
|
||||||
|
from in_record where arrival_id = #{arrivalId} for update
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="checkInNoUnique" parameterType="String" resultMap="InRecordResult">
|
<select id="checkInNoUnique" parameterType="String" resultMap="InRecordResult">
|
||||||
<include refid="selectInRecordVo"/> where ir.in_no = #{inNo} limit 1
|
<include refid="selectInRecordVo"/> where ir.in_no = #{inNo} limit 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<insert id="insertInRecord" parameterType="InRecord" useGeneratedKeys="true" keyProperty="id">
|
<insert id="insertInRecord" parameterType="InRecord" useGeneratedKeys="true" keyProperty="id">
|
||||||
insert into in_record(in_no, area_id, product_id, in_type, in_num, in_status, operator, in_operator, remark, create_time)
|
insert into in_record(in_no, area_id, product_id, purchase_request_id, arrival_id, source_no, in_type, in_num,
|
||||||
values(#{inNo}, #{areaId}, #{productId}, #{inType}, #{inNum}, #{inStatus}, #{operator}, #{inOperator}, #{remark}, sysdate())
|
in_status, operator, in_operator, remark, create_time)
|
||||||
|
values(#{inNo}, #{areaId}, #{productId}, #{purchaseRequestId}, #{arrivalId}, #{sourceNo}, #{inType}, #{inNum},
|
||||||
|
#{inStatus}, #{operator}, #{inOperator}, #{remark}, sysdate())
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<update id="updateInRecord" parameterType="InRecord">
|
<update id="updateInRecord" parameterType="InRecord">
|
||||||
@ -96,4 +109,8 @@
|
|||||||
delete from in_record where in_status = 0 and id in
|
delete from in_record where in_status = 0 and id in
|
||||||
<foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
|
<foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<delete id="deletePendingInRecordByArrivalId" parameterType="Long">
|
||||||
|
delete from in_record where arrival_id = #{arrivalId} and in_status = 0
|
||||||
|
</delete>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@ -0,0 +1,104 @@
|
|||||||
|
<?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.ruoyi.warehouse.mapper.PurchaseArrivalMapper">
|
||||||
|
<resultMap type="PurchaseArrival" id="PurchaseArrivalResult">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="arrivalNo" column="arrival_no"/>
|
||||||
|
<result property="purchaseRequestId" column="purchase_request_id"/>
|
||||||
|
<result property="arrivalQuantity" column="arrival_quantity"/>
|
||||||
|
<result property="arrivalDate" column="arrival_date"/>
|
||||||
|
<result property="acceptanceStatus" column="acceptance_status"/>
|
||||||
|
<result property="acceptanceRemark" column="acceptance_remark"/>
|
||||||
|
<result property="acceptedBy" column="accepted_by"/>
|
||||||
|
<result property="acceptedTime" column="accepted_time"/>
|
||||||
|
<result property="preAcceptPurchaseStatus" column="pre_accept_purchase_status"/>
|
||||||
|
<result property="revokedBy" column="revoked_by"/>
|
||||||
|
<result property="revokedTime" column="revoked_time"/>
|
||||||
|
<result property="revokeRemark" column="revoke_remark"/>
|
||||||
|
<result property="remark" column="remark"/>
|
||||||
|
<result property="createBy" column="create_by"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="updateBy" column="update_by"/>
|
||||||
|
<result property="updateTime" column="update_time"/>
|
||||||
|
<result property="requestNo" column="request_no"/>
|
||||||
|
<result property="applicantId" column="applicant_id"/>
|
||||||
|
<result property="applicantName" column="applicant_name"/>
|
||||||
|
<result property="productId" column="product_id"/>
|
||||||
|
<result property="productCode" column="product_code"/>
|
||||||
|
<result property="materialName" column="material_name"/>
|
||||||
|
<result property="materialBrand" column="material_brand"/>
|
||||||
|
<result property="materialParams" column="material_params"/>
|
||||||
|
<result property="unit" column="unit"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectPurchaseArrivalVo">
|
||||||
|
select pa.id, pa.arrival_no, pa.purchase_request_id, pa.arrival_quantity, pa.arrival_date,
|
||||||
|
pa.acceptance_status, pa.acceptance_remark, pa.accepted_by, pa.accepted_time,
|
||||||
|
pa.pre_accept_purchase_status, pa.revoked_by, pa.revoked_time, pa.revoke_remark,
|
||||||
|
pa.remark, pa.create_by, pa.create_time, pa.update_by, pa.update_time,
|
||||||
|
pr.request_no, pr.applicant_id, pr.applicant_name, pr.product_id,
|
||||||
|
pi.product_code, pr.material_name, pr.material_brand, pr.material_params, pr.unit
|
||||||
|
from purchase_arrival pa
|
||||||
|
join purchase_request pr on pr.id = pa.purchase_request_id
|
||||||
|
left join product_info pi on pi.id = pr.product_id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectPurchaseArrivalList" parameterType="Long" resultMap="PurchaseArrivalResult">
|
||||||
|
<include refid="selectPurchaseArrivalVo"/>
|
||||||
|
where pa.purchase_request_id = #{purchaseRequestId}
|
||||||
|
order by pa.id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseArrivalById" parameterType="Long" resultMap="PurchaseArrivalResult">
|
||||||
|
<include refid="selectPurchaseArrivalVo"/> where pa.id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseArrivalByIdForUpdate" parameterType="Long" resultMap="PurchaseArrivalResult">
|
||||||
|
<include refid="selectPurchaseArrivalVo"/> where pa.id = #{id} for update
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectActiveQuantityByRequestId" parameterType="Long" resultType="java.math.BigDecimal">
|
||||||
|
select coalesce(sum(arrival_quantity), 0)
|
||||||
|
from purchase_arrival
|
||||||
|
where purchase_request_id = #{purchaseRequestId} and acceptance_status in (0, 1)
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectAcceptedQuantityByRequestId" parameterType="Long" resultType="java.math.BigDecimal">
|
||||||
|
select coalesce(sum(arrival_quantity), 0)
|
||||||
|
from purchase_arrival
|
||||||
|
where purchase_request_id = #{purchaseRequestId} and acceptance_status = 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countByRequestId" parameterType="Long" resultType="int">
|
||||||
|
select count(*) from purchase_arrival where purchase_request_id = #{purchaseRequestId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertPurchaseArrival" parameterType="PurchaseArrival" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into purchase_arrival(arrival_no, purchase_request_id, arrival_quantity, arrival_date,
|
||||||
|
acceptance_status, remark, create_by, create_time)
|
||||||
|
values(#{arrivalNo}, #{purchaseRequestId}, #{arrivalQuantity}, #{arrivalDate}, 0,
|
||||||
|
#{remark}, #{createBy}, sysdate())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updatePurchaseArrivalAcceptance" parameterType="PurchaseArrival">
|
||||||
|
update purchase_arrival
|
||||||
|
set acceptance_status = #{acceptanceStatus}, acceptance_remark = #{acceptanceRemark},
|
||||||
|
accepted_by = #{acceptedBy}, accepted_time = #{acceptedTime},
|
||||||
|
pre_accept_purchase_status = #{preAcceptPurchaseStatus},
|
||||||
|
update_by = #{updateBy}, update_time = sysdate()
|
||||||
|
where id = #{id} and acceptance_status = 0
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="revokePurchaseArrivalAcceptance" parameterType="PurchaseArrival">
|
||||||
|
update purchase_arrival
|
||||||
|
set acceptance_status = 0,
|
||||||
|
acceptance_remark = null, accepted_by = null, accepted_time = null,
|
||||||
|
revoked_by = #{revokedBy}, revoked_time = #{revokedTime}, revoke_remark = #{revokeRemark},
|
||||||
|
update_by = #{updateBy}, update_time = sysdate()
|
||||||
|
where id = #{id} and acceptance_status in (1, 2)
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deletePendingPurchaseArrival" parameterType="Long">
|
||||||
|
delete from purchase_arrival where id = #{id} and acceptance_status = 0
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,107 @@
|
|||||||
|
<?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.ruoyi.warehouse.mapper.PurchaseBatchMapper">
|
||||||
|
<resultMap type="PurchaseBatch" id="PurchaseBatchResult">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="batchNo" column="batch_no"/>
|
||||||
|
<result property="batchName" column="batch_name"/>
|
||||||
|
<result property="purchaseStatus" column="purchase_status"/>
|
||||||
|
<result property="progressRemark" column="progress_remark"/>
|
||||||
|
<result property="requestCount" column="request_count"/>
|
||||||
|
<result property="createBy" column="create_by"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="updateBy" column="update_by"/>
|
||||||
|
<result property="updateTime" column="update_time"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<select id="selectPurchaseBatchList" parameterType="PurchaseBatch" resultMap="PurchaseBatchResult">
|
||||||
|
select pb.id, pb.batch_no, pb.batch_name, pb.purchase_status, pb.progress_remark,
|
||||||
|
pb.create_by, pb.create_time, pb.update_by, pb.update_time, count(pr.id) as request_count
|
||||||
|
from purchase_batch pb
|
||||||
|
left join purchase_request pr on pr.purchase_batch_id = pb.id
|
||||||
|
<where>
|
||||||
|
<if test="batchNo != null and batchNo != ''">and pb.batch_no like concat('%', #{batchNo}, '%')</if>
|
||||||
|
<if test="batchName != null and batchName != ''">and pb.batch_name like concat('%', #{batchName}, '%')</if>
|
||||||
|
<if test="purchaseStatus != null">and pb.purchase_status = #{purchaseStatus}</if>
|
||||||
|
</where>
|
||||||
|
group by pb.id, pb.batch_no, pb.batch_name, pb.purchase_status, pb.progress_remark,
|
||||||
|
pb.create_by, pb.create_time, pb.update_by, pb.update_time
|
||||||
|
order by pb.id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseBatchOptions" parameterType="String" resultMap="PurchaseBatchResult">
|
||||||
|
select pb.id, pb.batch_no, pb.batch_name, pb.purchase_status, pb.progress_remark,
|
||||||
|
pb.create_by, pb.create_time, pb.update_by, pb.update_time,
|
||||||
|
(select count(*) from purchase_request pr where pr.purchase_batch_id = pb.id) as request_count
|
||||||
|
from purchase_batch pb
|
||||||
|
where pb.purchase_status in (1, 2)
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (pb.batch_no like concat('%', #{keyword}, '%') or pb.batch_name like concat('%', #{keyword}, '%'))
|
||||||
|
</if>
|
||||||
|
order by pb.id desc
|
||||||
|
limit 100
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseBatchFilterOptions" parameterType="String" resultMap="PurchaseBatchResult">
|
||||||
|
select pb.id, pb.batch_no, pb.batch_name, pb.purchase_status, pb.progress_remark,
|
||||||
|
pb.create_by, pb.create_time, pb.update_by, pb.update_time,
|
||||||
|
(select count(*) from purchase_request pr where pr.purchase_batch_id = pb.id) as request_count
|
||||||
|
from purchase_batch pb
|
||||||
|
<where>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
and (pb.batch_no like concat('%', #{keyword}, '%') or pb.batch_name like concat('%', #{keyword}, '%'))
|
||||||
|
</if>
|
||||||
|
</where>
|
||||||
|
order by pb.id desc
|
||||||
|
limit 100
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseBatchById" parameterType="Long" resultMap="PurchaseBatchResult">
|
||||||
|
select id, batch_no, batch_name, purchase_status, progress_remark, create_by, create_time, update_by, update_time
|
||||||
|
from purchase_batch where id = #{id}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseBatchByNo" parameterType="String" resultMap="PurchaseBatchResult">
|
||||||
|
select id, batch_no, batch_name, purchase_status, progress_remark, create_by, create_time, update_by, update_time
|
||||||
|
from purchase_batch where batch_no = #{batchNo} limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertPurchaseBatch" parameterType="PurchaseBatch" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into purchase_batch(batch_no, batch_name, purchase_status, progress_remark, create_by, create_time)
|
||||||
|
values(#{batchNo}, #{batchName}, #{purchaseStatus}, #{progressRemark}, #{createBy}, sysdate())
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updatePurchaseBatch" parameterType="PurchaseBatch">
|
||||||
|
update purchase_batch
|
||||||
|
set batch_no = #{batchNo}, batch_name = #{batchName}, purchase_status = #{purchaseStatus},
|
||||||
|
progress_remark = #{progressRemark}, update_by = #{updateBy}, update_time = sysdate()
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<select id="countRequestsByBatchId" parameterType="Long" resultType="int">
|
||||||
|
select count(*) from purchase_request where purchase_batch_id = #{batchId}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="countRequestsByIds" parameterType="Long" resultType="int">
|
||||||
|
select count(*) from purchase_request where id in
|
||||||
|
<foreach collection="array" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<update id="assignRequests">
|
||||||
|
update purchase_request
|
||||||
|
set purchase_batch_id = #{batch.id}, purchase_batch_no = #{batch.batchNo},
|
||||||
|
update_by = #{updateBy}, update_time = sysdate()
|
||||||
|
where id in
|
||||||
|
<foreach collection="requestIds" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<update id="syncRequestBatchNo">
|
||||||
|
update purchase_request
|
||||||
|
set purchase_batch_no = #{batchNo}, update_by = #{updateBy}, update_time = sysdate()
|
||||||
|
where purchase_batch_id = #{batchId}
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deletePurchaseBatchById" parameterType="Long">
|
||||||
|
delete from purchase_batch where id = #{id}
|
||||||
|
</delete>
|
||||||
|
</mapper>
|
||||||
@ -0,0 +1,148 @@
|
|||||||
|
<?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.ruoyi.warehouse.mapper.PurchaseRequestMapper">
|
||||||
|
<resultMap type="PurchaseRequest" id="PurchaseRequestResult">
|
||||||
|
<id property="id" column="id"/>
|
||||||
|
<result property="requestNo" column="request_no"/>
|
||||||
|
<result property="productId" column="product_id"/>
|
||||||
|
<result property="purchaseBatchId" column="purchase_batch_id"/>
|
||||||
|
<result property="materialName" column="material_name"/>
|
||||||
|
<result property="materialBrand" column="material_brand"/>
|
||||||
|
<result property="category" column="category"/>
|
||||||
|
<result property="materialParams" column="material_params"/>
|
||||||
|
<result property="purchaseLink" column="purchase_link"/>
|
||||||
|
<result property="quantity" column="quantity"/>
|
||||||
|
<result property="unit" column="unit"/>
|
||||||
|
<result property="expectedDate" column="expected_date"/>
|
||||||
|
<result property="applicantId" column="applicant_id"/>
|
||||||
|
<result property="applicantName" column="applicant_name"/>
|
||||||
|
<result property="purchaseStatus" column="purchase_status"/>
|
||||||
|
<result property="purchaseBatchNo" column="purchase_batch_no"/>
|
||||||
|
<result property="progressRemark" column="progress_remark"/>
|
||||||
|
<result property="productRemark" column="product_remark"/>
|
||||||
|
<result property="requestRemark" column="remark"/>
|
||||||
|
<result property="remark" column="remark"/>
|
||||||
|
<result property="createBy" column="create_by"/>
|
||||||
|
<result property="createTime" column="create_time"/>
|
||||||
|
<result property="updateBy" column="update_by"/>
|
||||||
|
<result property="updateTime" column="update_time"/>
|
||||||
|
<result property="productCode" column="product_code"/>
|
||||||
|
<result property="productName" column="product_name"/>
|
||||||
|
<result property="productBrand" column="product_brand"/>
|
||||||
|
<result property="productCategory" column="product_category"/>
|
||||||
|
<result property="productSpec" column="product_spec"/>
|
||||||
|
<result property="acceptedQuantity" column="accepted_quantity"/>
|
||||||
|
<result property="pendingArrivalQuantity" column="pending_arrival_quantity"/>
|
||||||
|
<result property="remainingQuantity" column="remaining_quantity"/>
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<sql id="selectPurchaseRequestVo">
|
||||||
|
select pr.id, pr.request_no, pr.product_id, pr.purchase_batch_id, pr.material_name, pr.material_brand, pr.category, pr.material_params,
|
||||||
|
pr.purchase_link, pr.quantity, pr.unit, pr.expected_date, pr.applicant_id, pr.applicant_name,
|
||||||
|
pr.purchase_status, pr.purchase_batch_no, pr.progress_remark, pr.remark,
|
||||||
|
pr.create_by, pr.create_time, pr.update_by, pr.update_time,
|
||||||
|
pi.product_code, pi.product_name, pi.brand as product_brand,
|
||||||
|
pi.category as product_category, pi.spec as product_spec, pi.remark as product_remark,
|
||||||
|
coalesce(pa.accepted_quantity, 0) as accepted_quantity,
|
||||||
|
coalesce(pa.pending_quantity, 0) as pending_arrival_quantity,
|
||||||
|
greatest(pr.quantity - coalesce(pa.accepted_quantity, 0) - coalesce(pa.pending_quantity, 0), 0) as remaining_quantity
|
||||||
|
from purchase_request pr
|
||||||
|
left join product_info pi on pi.id = pr.product_id
|
||||||
|
left join (
|
||||||
|
select purchase_request_id,
|
||||||
|
sum(case when acceptance_status = 1 then arrival_quantity else 0 end) as accepted_quantity,
|
||||||
|
sum(case when acceptance_status = 0 then arrival_quantity else 0 end) as pending_quantity
|
||||||
|
from purchase_arrival
|
||||||
|
group by purchase_request_id
|
||||||
|
) pa on pa.purchase_request_id = pr.id
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="selectPurchaseRequestList" parameterType="PurchaseRequest" resultMap="PurchaseRequestResult">
|
||||||
|
<include refid="selectPurchaseRequestVo"/>
|
||||||
|
<where>
|
||||||
|
<if test="requestNo != null and requestNo != ''">and pr.request_no like concat('%', #{requestNo}, '%')</if>
|
||||||
|
<if test="productId != null">and pr.product_id = #{productId}</if>
|
||||||
|
<if test="materialName != null and materialName != ''">
|
||||||
|
and lower(concat_ws(' ', pr.material_name, pr.material_brand, pr.category, pr.material_params, pi.product_name, pi.product_code))
|
||||||
|
like concat('%', lower(#{materialName}), '%')
|
||||||
|
</if>
|
||||||
|
<if test="category != null and category != ''">and pr.category like concat('%', #{category}, '%')</if>
|
||||||
|
<if test="applicantId != null">and pr.applicant_id = #{applicantId}</if>
|
||||||
|
<if test="applicantName != null and applicantName != ''">and pr.applicant_name like concat('%', #{applicantName}, '%')</if>
|
||||||
|
<if test="purchaseStatus != null">and pr.purchase_status = #{purchaseStatus}</if>
|
||||||
|
<if test="purchaseBatchNo != null and purchaseBatchNo != ''">and pr.purchase_batch_no like concat('%', #{purchaseBatchNo}, '%')</if>
|
||||||
|
</where>
|
||||||
|
order by pr.id desc
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseRequestById" resultMap="PurchaseRequestResult">
|
||||||
|
<include refid="selectPurchaseRequestVo"/>
|
||||||
|
where pr.id = #{id}
|
||||||
|
<if test="applicantId != null">and pr.applicant_id = #{applicantId}</if>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectPurchaseRequestByIdForUpdate" parameterType="Long" resultMap="PurchaseRequestResult">
|
||||||
|
select pr.id, pr.request_no, pr.product_id, pr.purchase_batch_id, pr.material_name,
|
||||||
|
pr.material_brand, pr.category, pr.material_params, pr.purchase_link, pr.quantity,
|
||||||
|
pr.unit, pr.expected_date, pr.applicant_id, pr.applicant_name, pr.purchase_status,
|
||||||
|
pr.purchase_batch_no, pr.progress_remark, pr.remark, pr.create_by, pr.create_time,
|
||||||
|
pr.update_by, pr.update_time
|
||||||
|
from purchase_request pr where pr.id = #{id} for update
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="checkRequestNoUnique" parameterType="String" resultMap="PurchaseRequestResult">
|
||||||
|
<include refid="selectPurchaseRequestVo"/> where pr.request_no = #{requestNo} limit 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<insert id="insertPurchaseRequest" parameterType="PurchaseRequest" useGeneratedKeys="true" keyProperty="id">
|
||||||
|
insert into purchase_request(
|
||||||
|
request_no, product_id, material_name, material_brand, category, material_params, purchase_link,
|
||||||
|
quantity, unit, expected_date, applicant_id, applicant_name, purchase_status, purchase_batch_no,
|
||||||
|
progress_remark, remark, create_by, create_time
|
||||||
|
) values (
|
||||||
|
#{requestNo}, #{productId}, #{materialName}, #{materialBrand}, #{category}, #{materialParams}, #{purchaseLink},
|
||||||
|
#{quantity}, #{unit}, #{expectedDate}, #{applicantId}, #{applicantName}, #{purchaseStatus}, #{purchaseBatchNo},
|
||||||
|
#{progressRemark}, #{remark}, #{createBy}, sysdate()
|
||||||
|
)
|
||||||
|
</insert>
|
||||||
|
|
||||||
|
<update id="updatePurchaseRequest">
|
||||||
|
update purchase_request
|
||||||
|
<set>
|
||||||
|
<if test="purchaseRequest.requestNo != null and purchaseRequest.requestNo != ''">request_no = #{purchaseRequest.requestNo},</if>
|
||||||
|
product_id = #{purchaseRequest.productId},
|
||||||
|
<if test="purchaseRequest.materialName != null and purchaseRequest.materialName != ''">material_name = #{purchaseRequest.materialName},</if>
|
||||||
|
<if test="purchaseRequest.materialBrand != null">material_brand = #{purchaseRequest.materialBrand},</if>
|
||||||
|
<if test="purchaseRequest.category != null">category = #{purchaseRequest.category},</if>
|
||||||
|
<if test="purchaseRequest.materialParams != null">material_params = #{purchaseRequest.materialParams},</if>
|
||||||
|
<if test="purchaseRequest.purchaseLink != null">purchase_link = #{purchaseRequest.purchaseLink},</if>
|
||||||
|
<if test="purchaseRequest.quantity != null">quantity = #{purchaseRequest.quantity},</if>
|
||||||
|
<if test="purchaseRequest.unit != null">unit = #{purchaseRequest.unit},</if>
|
||||||
|
<if test="purchaseRequest.expectedDate != null">expected_date = #{purchaseRequest.expectedDate},</if>
|
||||||
|
<if test="purchaseRequest.applicantId != null">applicant_id = #{purchaseRequest.applicantId},</if>
|
||||||
|
<if test="purchaseRequest.applicantName != null and purchaseRequest.applicantName != ''">applicant_name = #{purchaseRequest.applicantName},</if>
|
||||||
|
<if test="purchaseRequest.purchaseStatus != null">purchase_status = #{purchaseRequest.purchaseStatus},</if>
|
||||||
|
<if test="purchaseRequest.purchaseBatchNo != null">purchase_batch_no = #{purchaseRequest.purchaseBatchNo},</if>
|
||||||
|
<if test="purchaseRequest.progressRemark != null">progress_remark = #{purchaseRequest.progressRemark},</if>
|
||||||
|
<if test="purchaseRequest.remark != null">remark = #{purchaseRequest.remark},</if>
|
||||||
|
<if test="purchaseRequest.updateBy != null">update_by = #{purchaseRequest.updateBy},</if>
|
||||||
|
update_time = sysdate()
|
||||||
|
</set>
|
||||||
|
where id = #{purchaseRequest.id}
|
||||||
|
<if test="applicantId != null">and applicant_id = #{applicantId}</if>
|
||||||
|
</update>
|
||||||
|
|
||||||
|
<delete id="deletePurchaseRequestByIds">
|
||||||
|
delete from purchase_request where id in
|
||||||
|
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
|
<if test="applicantId != null">and applicant_id = #{applicantId}</if>
|
||||||
|
</delete>
|
||||||
|
|
||||||
|
<update id="updatePurchaseStatus">
|
||||||
|
update purchase_request
|
||||||
|
set purchase_status = #{purchaseStatus},
|
||||||
|
progress_remark = #{progressRemark},
|
||||||
|
update_by = #{updateBy}, update_time = sysdate()
|
||||||
|
where id = #{id}
|
||||||
|
</update>
|
||||||
|
</mapper>
|
||||||
87
ruoyi-ui/src/api/warehouse/purchase.js
Normal file
87
ruoyi-ui/src/api/warehouse/purchase.js
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import request from '@/utils/request'
|
||||||
|
|
||||||
|
export function listPurchase(query) {
|
||||||
|
return request({ url: '/warehouse/purchase/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPurchase(id) {
|
||||||
|
return request({ url: '/warehouse/purchase/' + id, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPurchase(data) {
|
||||||
|
return request({ url: '/warehouse/purchase', method: 'post', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tableAddPurchase(data) {
|
||||||
|
return request({ url: '/warehouse/purchase/table', method: 'post', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function smartImportPurchase(data) {
|
||||||
|
return request({
|
||||||
|
url: '/warehouse/purchase/smart-import',
|
||||||
|
method: 'post',
|
||||||
|
data,
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data', repeatSubmit: false },
|
||||||
|
timeout: 300000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePurchase(data) {
|
||||||
|
return request({ url: '/warehouse/purchase', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePurchaseStatus(id, data) {
|
||||||
|
return request({ url: '/warehouse/purchase/' + id + '/status', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listPurchaseArrival(requestId) {
|
||||||
|
return request({ url: '/warehouse/purchase/arrival/' + requestId, method: 'get' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPurchaseArrival(data) {
|
||||||
|
return request({ url: '/warehouse/purchase/arrival', method: 'post', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function acceptPurchaseArrival(id, data) {
|
||||||
|
return request({ url: '/warehouse/purchase/arrival/' + id + '/accept', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function revokePurchaseArrival(id, data) {
|
||||||
|
return request({ url: '/warehouse/purchase/arrival/' + id + '/revoke', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delPurchaseArrival(id) {
|
||||||
|
return request({ url: '/warehouse/purchase/arrival/' + id, method: 'delete' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listPurchaseBatch(query) {
|
||||||
|
return request({ url: '/warehouse/purchase/batch/list', method: 'get', params: query })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function optionPurchaseBatch(keyword) {
|
||||||
|
return request({ url: '/warehouse/purchase/batch/options', method: 'get', params: { keyword } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterOptionPurchaseBatch(keyword) {
|
||||||
|
return request({ url: '/warehouse/purchase/batch/filter-options', method: 'get', params: { keyword } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addPurchaseBatch(data) {
|
||||||
|
return request({ url: '/warehouse/purchase/batch', method: 'post', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePurchaseBatch(data) {
|
||||||
|
return request({ url: '/warehouse/purchase/batch', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assignPurchaseBatch(data) {
|
||||||
|
return request({ url: '/warehouse/purchase/batch/assign', method: 'put', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delPurchaseBatch(id) {
|
||||||
|
return request({ url: '/warehouse/purchase/batch/' + id, method: 'delete' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function delPurchase(ids) {
|
||||||
|
return request({ url: '/warehouse/purchase/' + ids, method: 'delete' })
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :title="title" :visible.sync="visible" :width="width" append-to-body @close="handleClose">
|
<el-dialog :title="title" :visible.sync="visible" :width="width" append-to-body @close="handleClose">
|
||||||
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="uploadUrl" :disabled="isUploading" :on-progress="handleProgress" :on-success="handleSuccess" :auto-upload="false" drag>
|
<el-upload ref="uploadRef" :limit="1" accept=".xlsx, .xls" :headers="headers" :action="uploadUrl" :disabled="isUploading" :on-progress="handleProgress" :on-success="handleSuccess" :on-error="handleError" :auto-upload="false" drag>
|
||||||
<i class="el-icon-upload"></i>
|
<i class="el-icon-upload"></i>
|
||||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||||
<div class="el-upload__tip text-center" slot="tip">
|
<div class="el-upload__tip text-center" slot="tip">
|
||||||
<div class="el-upload__tip" slot="tip">
|
<div v-if="showUpdateSupport" class="el-upload__tip" slot="tip">
|
||||||
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
|
<el-checkbox v-model="updateSupport"> {{ updateSupportLabel }} </el-checkbox>
|
||||||
</div>
|
</div>
|
||||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||||
@ -52,6 +52,10 @@ export default {
|
|||||||
updateSupportLabel: {
|
updateSupportLabel: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '是否更新已经存在的数据'
|
default: '是否更新已经存在的数据'
|
||||||
|
},
|
||||||
|
showUpdateSupport: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -99,14 +103,29 @@ export default {
|
|||||||
},
|
},
|
||||||
// 上传成功
|
// 上传成功
|
||||||
handleSuccess(response) {
|
handleSuccess(response) {
|
||||||
this.visible = false
|
|
||||||
this.isUploading = false
|
this.isUploading = false
|
||||||
|
if (!response || response.code !== 200) {
|
||||||
|
this.$modal.msgError((response && response.msg) || '导入失败,请检查文件内容。')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.visible = false
|
||||||
if (this.$refs.uploadRef) {
|
if (this.$refs.uploadRef) {
|
||||||
this.$refs.uploadRef.clearFiles()
|
this.$refs.uploadRef.clearFiles()
|
||||||
}
|
}
|
||||||
this.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
|
this.$alert("<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>" + response.msg + '</div>', '导入结果', { dangerouslyUseHTMLString: true })
|
||||||
this.$emit('success')
|
this.$emit('success')
|
||||||
},
|
},
|
||||||
|
handleError(error) {
|
||||||
|
this.isUploading = false
|
||||||
|
let message = '导入失败,请检查网络、权限或文件格式。'
|
||||||
|
if (error && error.message) {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(error.message)
|
||||||
|
message = response.msg || message
|
||||||
|
} catch (ignored) {}
|
||||||
|
}
|
||||||
|
this.$modal.msgError(message)
|
||||||
|
},
|
||||||
// 提交上传
|
// 提交上传
|
||||||
handleSubmit() {
|
handleSubmit() {
|
||||||
const files = this.$refs.uploadRef.uploadFiles
|
const files = this.$refs.uploadRef.uploadFiles
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
<p>面向库区、产品、入库、出库和盘库的日常库存作业台。</p>
|
<p>面向库区、产品、入库、出库和盘库的日常库存作业台。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
|
<el-button type="primary" icon="el-icon-top-right" @click="$router.push('/warehouse/outRecord')" v-hasPermi="['warehouse:out:list']">出库登记</el-button>
|
||||||
|
<el-button type="success" icon="el-icon-shopping-cart-2" @click="$router.push('/purchase')" v-hasPermi="['warehouse:purchase:list']">采购申请</el-button>
|
||||||
<el-button type="primary" icon="el-icon-upload2" @click="$router.push('/warehouse/batch-import')">批量导入</el-button>
|
<el-button type="primary" icon="el-icon-upload2" @click="$router.push('/warehouse/batch-import')">批量导入</el-button>
|
||||||
<el-button type="primary" plain icon="el-icon-mobile-phone" @click="$router.push('/mobile')">移动端入口</el-button>
|
<el-button type="primary" plain icon="el-icon-mobile-phone" @click="$router.push('/mobile')">移动端入口</el-button>
|
||||||
</div>
|
</div>
|
||||||
@ -209,11 +211,14 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.agent.loading = true
|
this.agent.loading = true
|
||||||
analyzeAgent({ text, contextResults: this.agent.contextResults }).then(res => {
|
const usesContext = this.hasExplicitAgentReference(text)
|
||||||
|
analyzeAgent({ text, contextResults: usesContext ? this.agent.contextResults : [] }).then(res => {
|
||||||
this.agent.plan = res.data
|
this.agent.plan = res.data
|
||||||
this.agent.text = ''
|
this.agent.text = ''
|
||||||
if (res.data && res.data.results && res.data.results.length) {
|
if (res.data && res.data.results && res.data.results.length) {
|
||||||
this.agent.contextResults = res.data.results
|
this.agent.contextResults = res.data.results
|
||||||
|
} else if (!usesContext) {
|
||||||
|
this.agent.contextResults = []
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.agent.loading = false
|
this.agent.loading = false
|
||||||
@ -229,6 +234,7 @@ export default {
|
|||||||
this.$modal.msgSuccess(res.data.message || '操作完成')
|
this.$modal.msgSuccess(res.data.message || '操作完成')
|
||||||
this.loadDashboard()
|
this.loadDashboard()
|
||||||
this.agent.text = ''
|
this.agent.text = ''
|
||||||
|
this.agent.contextResults = []
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.agent.executing = false
|
this.agent.executing = false
|
||||||
})
|
})
|
||||||
@ -236,6 +242,10 @@ export default {
|
|||||||
clearAgent() {
|
clearAgent() {
|
||||||
this.agent.text = ''
|
this.agent.text = ''
|
||||||
this.agent.plan = null
|
this.agent.plan = null
|
||||||
|
this.agent.contextResults = []
|
||||||
|
},
|
||||||
|
hasExplicitAgentReference(text) {
|
||||||
|
return /第\s*[0-9一二两三四五六七八九十]+\s*(个|条|项)?|上一个|上一条|刚才(?:的)?(?:那个|那条|那项)|刚刚(?:的)?(?:那个|那条|那项)|这个|那个|它|上述|上面(?:的)?/.test(text || '')
|
||||||
},
|
},
|
||||||
canSelectAgentResult(plan) {
|
canSelectAgentResult(plan) {
|
||||||
return plan && ['create_out_record', 'create_in_record', 'create_stock_check'].includes(plan.action) && plan.results && plan.results.length
|
return plan && ['create_out_record', 'create_in_record', 'create_stock_check'].includes(plan.action) && plan.results && plan.results.length
|
||||||
@ -342,6 +352,7 @@ export default {
|
|||||||
}
|
}
|
||||||
.hero-actions {
|
.hero-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@ -643,11 +643,14 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.agent.loading = true
|
this.agent.loading = true
|
||||||
analyzeAgent({ text, contextResults: this.agent.contextResults }).then(res => {
|
const usesContext = this.hasExplicitAgentReference(text)
|
||||||
|
analyzeAgent({ text, contextResults: usesContext ? this.agent.contextResults : [] }).then(res => {
|
||||||
this.agent.plan = res.data
|
this.agent.plan = res.data
|
||||||
this.agent.text = ''
|
this.agent.text = ''
|
||||||
if (res.data && res.data.results && res.data.results.length) {
|
if (res.data && res.data.results && res.data.results.length) {
|
||||||
this.agent.contextResults = res.data.results
|
this.agent.contextResults = res.data.results
|
||||||
|
} else if (!usesContext) {
|
||||||
|
this.agent.contextResults = []
|
||||||
}
|
}
|
||||||
this.autoFillAgentForm(res.data)
|
this.autoFillAgentForm(res.data)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
@ -664,6 +667,7 @@ export default {
|
|||||||
this.$modal.msgSuccess(res.data.message || '操作完成')
|
this.$modal.msgSuccess(res.data.message || '操作完成')
|
||||||
this.refreshAll()
|
this.refreshAll()
|
||||||
this.agent.text = ''
|
this.agent.text = ''
|
||||||
|
this.agent.contextResults = []
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.agent.executing = false
|
this.agent.executing = false
|
||||||
})
|
})
|
||||||
@ -672,6 +676,10 @@ export default {
|
|||||||
this.agent.text = ''
|
this.agent.text = ''
|
||||||
this.agent.plan = null
|
this.agent.plan = null
|
||||||
this.agent.pendingNextPlan = null
|
this.agent.pendingNextPlan = null
|
||||||
|
this.agent.contextResults = []
|
||||||
|
},
|
||||||
|
hasExplicitAgentReference(text) {
|
||||||
|
return /第\s*[0-9一二两三四五六七八九十]+\s*(个|条|项)?|上一个|上一条|刚才(?:的)?(?:那个|那条|那项)|刚刚(?:的)?(?:那个|那条|那项)|这个|那个|它|上述|上面(?:的)?/.test(text || '')
|
||||||
},
|
},
|
||||||
canSelectAgentResult(plan) {
|
canSelectAgentResult(plan) {
|
||||||
return plan && ['create_out_record', 'create_in_record', 'create_stock_check'].includes(plan.action) && plan.results && plan.results.length
|
return plan && ['create_out_record', 'create_in_record', 'create_stock_check'].includes(plan.action) && plan.results && plan.results.length
|
||||||
|
|||||||
@ -49,7 +49,13 @@
|
|||||||
<el-table v-loading="loading" :data="recordList" @selection-change="handleSelectionChange">
|
<el-table v-loading="loading" :data="recordList" @selection-change="handleSelectionChange">
|
||||||
<el-table-column type="selection" width="55" align="center" />
|
<el-table-column type="selection" width="55" align="center" />
|
||||||
<el-table-column label="入库单号" align="center" prop="inNo" min-width="150" />
|
<el-table-column label="入库单号" align="center" prop="inNo" min-width="150" />
|
||||||
<el-table-column label="库区" align="center" prop="areaName" min-width="110" />
|
<el-table-column label="库区" align="center" prop="areaName" min-width="110">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.areaId">{{ scope.row.areaName || scope.row.areaCode }}</span>
|
||||||
|
<el-tag v-else type="warning" size="mini">待分配</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="来源单号" align="center" prop="sourceNo" min-width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="产品信息" min-width="280">
|
<el-table-column label="产品信息" min-width="280">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<div class="product-name">{{ scope.row.productName || '-' }}</div>
|
<div class="product-name">{{ scope.row.productName || '-' }}</div>
|
||||||
@ -77,7 +83,7 @@
|
|||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-button v-if="scope.row.inStatus === 0" size="mini" type="text" icon="el-icon-check" @click="handleConfirm(scope.row)" v-hasPermi="['warehouse:in:confirm']">入库</el-button>
|
<el-button v-if="scope.row.inStatus === 0" size="mini" type="text" icon="el-icon-check" @click="handleConfirm(scope.row)" v-hasPermi="['warehouse:in:confirm']">入库</el-button>
|
||||||
<el-button v-if="scope.row.inStatus === 0" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['warehouse:in:edit']">修改</el-button>
|
<el-button v-if="scope.row.inStatus === 0" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['warehouse:in:edit']">修改</el-button>
|
||||||
<el-button v-if="scope.row.inStatus === 0" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['warehouse:in:remove']">删除</el-button>
|
<el-button v-if="scope.row.inStatus === 0 && !scope.row.arrivalId" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['warehouse:in:remove']">删除</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@ -86,14 +92,14 @@
|
|||||||
|
|
||||||
<el-dialog :title="title" :visible.sync="open" width="620px" append-to-body>
|
<el-dialog :title="title" :visible.sync="open" width="620px" append-to-body>
|
||||||
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
<el-form ref="form" :model="form" :rules="rules" label-width="100px">
|
||||||
<el-form-item label="入库单号" prop="inNo"><el-input v-model="form.inNo" placeholder="不填则自动生成" /></el-form-item>
|
<el-form-item label="入库单号" prop="inNo"><el-input v-model="form.inNo" :disabled="!!form.arrivalId" placeholder="不填则自动生成" /></el-form-item>
|
||||||
<el-form-item label="库区" prop="areaId">
|
<el-form-item label="库区" prop="areaId">
|
||||||
<el-select v-model="form.areaId" placeholder="请选择库区" filterable>
|
<el-select v-model="form.areaId" placeholder="请选择库区" filterable>
|
||||||
<el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" />
|
<el-option v-for="item in areaOptions" :key="item.id" :label="areaLabel(item)" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="产品" prop="productId">
|
<el-form-item label="产品" prop="productId">
|
||||||
<el-select v-model="form.productId" placeholder="请选择产品" filterable>
|
<el-select v-model="form.productId" :disabled="!!form.arrivalId" placeholder="请选择产品" filterable>
|
||||||
<el-option v-for="item in productOptions" :key="item.id" :label="productLabel(item)" :value="item.id" />
|
<el-option v-for="item in productOptions" :key="item.id" :label="productLabel(item)" :value="item.id" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -103,11 +109,11 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="入库类型" prop="inType">
|
<el-form-item label="入库类型" prop="inType">
|
||||||
<el-select v-model="form.inType" placeholder="请选择入库类型">
|
<el-select v-model="form.inType" :disabled="!!form.arrivalId" placeholder="请选择入库类型">
|
||||||
<el-option v-for="dict in dict.type.wms_in_type" :key="dict.value" :label="dict.label" :value="Number(dict.value)" />
|
<el-option v-for="dict in dict.type.wms_in_type" :key="dict.value" :label="dict.label" :value="Number(dict.value)" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="入库数量" prop="inNum"><el-input-number v-model="form.inNum" :min="0.001" :precision="3" :step="1" controls-position="right" /></el-form-item>
|
<el-form-item label="入库数量" prop="inNum"><el-input-number v-model="form.inNum" :disabled="!!form.arrivalId" :min="0.001" :precision="3" :step="1" controls-position="right" /></el-form-item>
|
||||||
<el-form-item label="备注" prop="remark"><el-input v-model="form.remark" type="textarea" placeholder="请输入备注" /></el-form-item>
|
<el-form-item label="备注" prop="remark"><el-input v-model="form.remark" type="textarea" placeholder="请输入备注" /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div slot="footer" class="dialog-footer">
|
<div slot="footer" class="dialog-footer">
|
||||||
@ -230,6 +236,11 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
handleConfirm(row) {
|
handleConfirm(row) {
|
||||||
|
if (!row.areaId || !row.inOperator) {
|
||||||
|
this.$modal.msgWarning('请先完善库区和入库人')
|
||||||
|
this.handleUpdate(row)
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$modal.confirm('是否确认入库单号为"' + row.inNo + '"的记录?').then(() => confirmInRecord(row.id)).then(() => {
|
this.$modal.confirm('是否确认入库单号为"' + row.inNo + '"的记录?').then(() => confirmInRecord(row.id)).then(() => {
|
||||||
this.getList()
|
this.getList()
|
||||||
this.$modal.msgSuccess('入库成功')
|
this.$modal.msgSuccess('入库成功')
|
||||||
|
|||||||
945
ruoyi-ui/src/views/warehouse/purchase/index.vue
Normal file
945
ruoyi-ui/src/views/warehouse/purchase/index.vue
Normal file
@ -0,0 +1,945 @@
|
|||||||
|
<template>
|
||||||
|
<div class="app-container">
|
||||||
|
<el-form ref="queryForm" :model="queryParams" size="small" :inline="true" v-show="showSearch" label-width="80px">
|
||||||
|
<el-form-item label="物料" prop="materialName">
|
||||||
|
<el-input v-model="queryParams.materialName" placeholder="名称、参数或物料编码" clearable @keyup.enter.native="handleQuery" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="采购状态" prop="purchaseStatus">
|
||||||
|
<el-select v-model="queryParams.purchaseStatus" placeholder="请选择状态" clearable>
|
||||||
|
<el-option v-for="item in statusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="采购批次" prop="purchaseBatchNo">
|
||||||
|
<el-select v-model="queryParams.purchaseBatchNo" filterable remote clearable reserve-keyword :remote-method="searchBatchFilterOptions" :loading="batchFilterLoading" placeholder="输入编号或名称搜索批次">
|
||||||
|
<el-option v-for="item in batchFilterOptions" :key="item.id" :label="batchLabel(item)" :value="item.batchNo" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="申请人" prop="applicantName">
|
||||||
|
<el-select v-model="queryParams.applicantName" filterable clearable placeholder="请选择申请人">
|
||||||
|
<el-option v-for="item in userOptions" :key="item.userId" :label="userLabel(item)" :value="item.userName" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<el-row :gutter="10" class="mb8">
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd" v-hasPermi="['warehouse:purchase:add', 'warehouse:purchase:manage']">新增申请</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="el-icon-s-grid" size="mini" @click="handleGridAdd" v-hasPermi="['warehouse:purchase:add', 'warehouse:purchase:manage']">表格录入</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="primary" plain icon="el-icon-magic-stick" size="mini" @click="handleSmartImport" v-hasPermi="['warehouse:purchase:add', 'warehouse:purchase:manage']">智能导入</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="success" plain icon="el-icon-edit" size="mini" :disabled="single" @click="handleUpdate" v-hasPermi="['warehouse:purchase:edit', 'warehouse:purchase:manage']">修改</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="danger" plain icon="el-icon-delete" size="mini" :disabled="multiple" @click="handleDelete" v-hasPermi="['warehouse:purchase:remove', 'warehouse:purchase:manage']">删除</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="warning" plain icon="el-icon-upload2" size="mini" @click="handleImport" v-hasPermi="['warehouse:purchase:import', 'warehouse:purchase:manage']">导入</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" v-hasPermi="['warehouse:purchase:export', 'warehouse:purchase:manage']">导出</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-if="canManageAll" type="primary" plain icon="el-icon-sort" size="mini" :disabled="multiple" @click="handleAssignBatch" v-hasPermi="['warehouse:purchase:manage']">分配/转批次</el-button>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="1.5">
|
||||||
|
<el-button v-if="canManageAll" type="primary" plain icon="el-icon-s-operation" size="mini" @click="handleBatchManage" v-hasPermi="['warehouse:purchase:manage']">批次管理</el-button>
|
||||||
|
</el-col>
|
||||||
|
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
||||||
|
</el-row>
|
||||||
|
|
||||||
|
<el-table v-loading="loading" :data="purchaseList" @selection-change="handleSelectionChange">
|
||||||
|
<el-table-column type="selection" width="55" align="center" :selectable="canOperate" />
|
||||||
|
<el-table-column label="申请人" prop="applicantName" width="110" />
|
||||||
|
<el-table-column label="物料名称" prop="materialName" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="品牌" prop="materialBrand" width="110" show-overflow-tooltip />
|
||||||
|
<el-table-column label="关联库房物料" min-width="170" show-overflow-tooltip>
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span v-if="scope.row.productId">{{ scope.row.productCode }} / {{ scope.row.productName }}</span>
|
||||||
|
<el-tag v-else size="mini" type="info">未关联</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="类别" prop="category" width="110" show-overflow-tooltip />
|
||||||
|
<el-table-column label="参数规格" prop="materialParams" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="采购链接" width="90" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-link v-if="scope.row.purchaseLink" :href="scope.row.purchaseLink" target="_blank" rel="noopener noreferrer" type="primary">打开</el-link>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="数量" width="110" align="right">
|
||||||
|
<template slot-scope="scope">{{ scope.row.quantity }} {{ scope.row.unit || '' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="到货验收" min-width="190">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<div>已验收 {{ scope.row.acceptedQuantity || 0 }} / {{ scope.row.quantity }}</div>
|
||||||
|
<div class="arrival-summary" v-if="Number(scope.row.pendingArrivalQuantity) > 0">待验收 {{ scope.row.pendingArrivalQuantity }} {{ scope.row.unit || '' }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="采购状态" width="100" align="center">
|
||||||
|
<template slot-scope="scope"><el-tag :type="statusType(scope.row.purchaseStatus)" size="mini">{{ statusLabel(scope.row.purchaseStatus) }}</el-tag></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="采购批次" prop="purchaseBatchNo" width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column label="期望日期" prop="expectedDate" width="110" />
|
||||||
|
<el-table-column label="创建时间" prop="createTime" width="160">
|
||||||
|
<template slot-scope="scope">{{ parseTime(scope.row.createTime) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" align="center" width="285" fixed="right" class-name="small-padding fixed-width">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button v-if="canOperate(scope.row)" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)" v-hasPermi="['warehouse:purchase:edit', 'warehouse:purchase:manage']">修改</el-button>
|
||||||
|
<el-button size="mini" type="text" icon="el-icon-box" @click="handleArrival(scope.row)" v-hasPermi="['warehouse:purchase:list', 'warehouse:purchase:manage']">到货</el-button>
|
||||||
|
<el-button v-if="canOperate(scope.row) && Number(scope.row.acceptedQuantity || 0) === 0" size="mini" type="text" icon="el-icon-refresh" @click="handleStatus(scope.row)" v-hasPermi="['warehouse:purchase:edit', 'warehouse:purchase:manage']">状态</el-button>
|
||||||
|
<el-button v-if="canManageAll" size="mini" type="text" icon="el-icon-sort" @click="handleAssignBatch(scope.row)" v-hasPermi="['warehouse:purchase:manage']">转批次</el-button>
|
||||||
|
<el-button v-if="canOperate(scope.row)" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)" v-hasPermi="['warehouse:purchase:remove', 'warehouse:purchase:manage']">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
|
||||||
|
|
||||||
|
<el-dialog :title="title" :visible.sync="open" width="720px" custom-class="purchase-dialog" append-to-body>
|
||||||
|
<el-form ref="form" :model="form" :rules="rules" label-width="105px">
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-form-item label="已有产品" prop="productId">
|
||||||
|
<el-select v-model="form.productId" filterable clearable placeholder="选择后自动填充;不选择则按下方信息复用或新增" @change="handleExistingProductChange">
|
||||||
|
<el-option v-for="item in productOptions" :key="item.id" :label="productLabel(item)" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="产品名称" prop="materialName">
|
||||||
|
<el-select v-model="form.materialName" filterable clearable allow-create default-first-option placeholder="请选择或输入产品名称" @change="handleMaterialNameChange">
|
||||||
|
<el-option v-for="item in productNameOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="品牌" prop="materialBrand">
|
||||||
|
<el-select v-model="form.materialBrand" filterable clearable allow-create default-first-option placeholder="请选择或输入品牌" @change="syncProductId">
|
||||||
|
<el-option v-for="item in brandOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="分类" prop="category">
|
||||||
|
<el-select v-model="form.category" filterable clearable allow-create default-first-option placeholder="请选择或输入分类" @change="syncProductId">
|
||||||
|
<el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="规格型号" prop="materialParams">
|
||||||
|
<el-select v-model="form.materialParams" filterable clearable allow-create default-first-option placeholder="请选择或输入规格型号" @change="syncProductId">
|
||||||
|
<el-option v-for="item in specOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<el-form-item label="单位" prop="unit">
|
||||||
|
<el-select v-model="form.unit" filterable clearable allow-create default-first-option placeholder="单位" @change="syncProductId">
|
||||||
|
<el-option v-for="item in unitOptions" :key="item" :label="item" :value="item" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="需求数量" prop="quantity"><el-input-number v-model="form.quantity" :min="0.001" :precision="3" :controls="false" /></el-form-item></el-col>
|
||||||
|
<el-col :span="12"><el-form-item label="期望到货日期" prop="expectedDate"><el-date-picker v-model="form.expectedDate" type="date" value-format="yyyy-MM-dd" placeholder="请选择日期" /></el-form-item></el-col>
|
||||||
|
<el-col :span="24"><el-form-item label="产品备注" prop="productRemark"><el-input v-model="form.productRemark" placeholder="新建产品时写入产品信息,复用已有产品时不覆盖" /></el-form-item></el-col>
|
||||||
|
<el-col :span="24"><el-form-item label="采购链接" prop="purchaseLink"><el-input v-model="form.purchaseLink" placeholder="请输入商品或供应商链接" /></el-form-item></el-col>
|
||||||
|
<el-col :span="24"><el-form-item label="申请备注" prop="remark"><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入用途、预算等补充信息" /></el-form-item></el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitForm">确定</el-button>
|
||||||
|
<el-button @click="cancel">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="分配或转移采购批次" :visible.sync="assignOpen" width="520px" append-to-body>
|
||||||
|
<el-form ref="assignForm" :model="assignForm" :rules="assignRules" label-width="95px">
|
||||||
|
<el-form-item label="目标批次" prop="batchId">
|
||||||
|
<el-select v-model="assignForm.batchId" filterable remote :remote-method="searchBatchOptions" :loading="batchOptionLoading" placeholder="输入编号或名称搜索可用批次">
|
||||||
|
<el-option v-for="item in batchOptions" :key="item.id" :label="batchLabel(item)" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitAssignBatch">确定</el-button>
|
||||||
|
<el-button @click="assignOpen = false">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="采购批次管理" :visible.sync="batchOpen" width="820px" append-to-body>
|
||||||
|
<el-form ref="batchQueryForm" :model="batchQuery" size="small" :inline="true" label-width="70px">
|
||||||
|
<el-form-item label="批次" prop="batchNo"><el-input v-model="batchQuery.batchNo" clearable placeholder="批次编号" @keyup.enter.native="handleBatchQuery" /></el-form-item>
|
||||||
|
<el-form-item label="名称" prop="batchName"><el-input v-model="batchQuery.batchName" clearable placeholder="批次名称" @keyup.enter.native="handleBatchQuery" /></el-form-item>
|
||||||
|
<el-form-item label="状态" prop="purchaseStatus">
|
||||||
|
<el-select v-model="batchQuery.purchaseStatus" clearable placeholder="全部状态">
|
||||||
|
<el-option v-for="item in batchStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleBatchQuery">搜索</el-button>
|
||||||
|
<el-button icon="el-icon-refresh" size="mini" @click="resetBatchQuery">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" class="mb8" @click="handleAddBatch">新增批次</el-button>
|
||||||
|
<el-table v-loading="batchLoading" :data="batchList" max-height="420">
|
||||||
|
<el-table-column label="批次编号" prop="batchNo" min-width="140" />
|
||||||
|
<el-table-column label="批次名称" prop="batchName" min-width="150" show-overflow-tooltip />
|
||||||
|
<el-table-column label="批次状态" width="100" align="center">
|
||||||
|
<template slot-scope="scope"><el-tag :type="batchStatusType(scope.row.purchaseStatus)" size="mini">{{ batchStatusLabel(scope.row.purchaseStatus) }}</el-tag></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="产品数" prop="requestCount" width="80" align="center" />
|
||||||
|
<el-table-column label="批次说明" prop="progressRemark" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="操作" width="130" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-button type="text" icon="el-icon-edit" @click="handleEditBatch(scope.row)">修改</el-button>
|
||||||
|
<el-button type="text" icon="el-icon-delete" @click="handleDeleteBatch(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<pagination v-show="batchTotal > 0" :total="batchTotal" :page.sync="batchQuery.pageNum" :limit.sync="batchQuery.pageSize" @pagination="getBatchList" />
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog :title="batchTitle" :visible.sync="batchFormOpen" width="540px" append-to-body>
|
||||||
|
<el-form ref="batchForm" :model="batchForm" :rules="batchRules" label-width="95px">
|
||||||
|
<el-form-item label="批次编号" prop="batchNo"><el-input v-model="batchForm.batchNo" placeholder="如:PC202609-01" /></el-form-item>
|
||||||
|
<el-form-item label="批次名称" prop="batchName"><el-input v-model="batchForm.batchName" placeholder="请输入批次名称" /></el-form-item>
|
||||||
|
<el-form-item label="批次状态" prop="purchaseStatus">
|
||||||
|
<el-select v-model="batchForm.purchaseStatus" placeholder="请选择批次状态">
|
||||||
|
<el-option v-for="item in batchStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="批次说明" prop="progressRemark"><el-input v-model="batchForm.progressRemark" type="textarea" :rows="3" placeholder="请输入批次层面的说明" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" @click="submitBatch">确定</el-button>
|
||||||
|
<el-button @click="batchFormOpen = false">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="采购申请智能导入" :visible.sync="smartImportOpen" width="560px" custom-class="purchase-smart-dialog" append-to-body @closed="handleSmartImportClosed">
|
||||||
|
<el-upload ref="smartUpload" drag action="#" :auto-upload="false" :limit="1" :file-list="smartFileList" accept=".xls,.xlsx,.png,.jpg,.jpeg,.webp" :on-change="handleSmartFileChange" :on-remove="handleSmartFileRemove">
|
||||||
|
<i class="el-icon-upload" />
|
||||||
|
<div class="el-upload__text">拖入或选择采购清单</div>
|
||||||
|
<div slot="tip" class="el-upload__tip">支持 Excel、表格截图和清单图片,分析结果会先进入可编辑表格</div>
|
||||||
|
</el-upload>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" icon="el-icon-magic-stick" :loading="smartImportLoading" :disabled="!smartFile" @click="submitSmartImport">开始分析</el-button>
|
||||||
|
<el-button @click="smartImportOpen = false">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="采购申请表格录入" :visible.sync="gridOpen" width="95%" custom-class="purchase-grid-dialog" append-to-body>
|
||||||
|
<div class="grid-toolbar mb8">
|
||||||
|
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="addGridRow">新增行</el-button>
|
||||||
|
<el-button icon="el-icon-delete" size="mini" :disabled="gridRows.length <= 1" @click="removeEmptyGridRows">清理空行</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table :data="gridRows" border height="470" class="purchase-grid">
|
||||||
|
<el-table-column type="index" label="#" width="48" fixed="left" />
|
||||||
|
<el-table-column v-if="gridRows.some(row => row.smartTip)" label="识别提示" min-width="210">
|
||||||
|
<template slot-scope="scope"><span class="smart-tip">{{ scope.row.smartTip || '-' }}</span></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="关联物料编码" min-width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<el-select v-model="scope.row.productCode" filterable clearable allow-create default-first-option @change="handleGridCodeChange(scope.row)" @paste.native="handleGridPaste($event, scope.$index, 0)">
|
||||||
|
<el-option v-for="item in productCodeOptions" :key="item.id" :label="item.productCode + ' / ' + item.productName" :value="item.productCode" />
|
||||||
|
</el-select>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="物料名称" min-width="170">
|
||||||
|
<template slot-scope="scope"><el-select v-model="scope.row.materialName" filterable allow-create default-first-option @change="handleGridNameChange(scope.row)" @paste.native="handleGridPaste($event, scope.$index, 1)"><el-option v-for="item in productNameOptions" :key="item" :label="item" :value="item" /></el-select></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="品牌" min-width="130">
|
||||||
|
<template slot-scope="scope"><el-select v-model="scope.row.materialBrand" filterable clearable allow-create default-first-option @paste.native="handleGridPaste($event, scope.$index, 2)"><el-option v-for="item in brandOptions" :key="item" :label="item" :value="item" /></el-select></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="分类" min-width="130">
|
||||||
|
<template slot-scope="scope"><el-select v-model="scope.row.category" filterable clearable allow-create default-first-option @paste.native="handleGridPaste($event, scope.$index, 3)"><el-option v-for="item in categoryOptions" :key="item" :label="item" :value="item" /></el-select></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="参数规格" min-width="160">
|
||||||
|
<template slot-scope="scope"><el-select v-model="scope.row.materialParams" filterable clearable allow-create default-first-option @paste.native="handleGridPaste($event, scope.$index, 4)"><el-option v-for="item in specOptions" :key="item" :label="item" :value="item" /></el-select></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="单位" min-width="100">
|
||||||
|
<template slot-scope="scope"><el-select v-model="scope.row.unit" filterable clearable allow-create default-first-option @paste.native="handleGridPaste($event, scope.$index, 5)"><el-option v-for="item in unitOptions" :key="item" :label="item" :value="item" /></el-select></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="需求数量" min-width="110">
|
||||||
|
<template slot-scope="scope"><el-input v-model="scope.row.quantity" @paste.native="handleGridPaste($event, scope.$index, 6)" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="期望到货日期" min-width="140">
|
||||||
|
<template slot-scope="scope"><el-date-picker v-model="scope.row.expectedDate" type="date" value-format="yyyy-MM-dd" @paste.native="handleGridPaste($event, scope.$index, 7)" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="采购链接" min-width="210">
|
||||||
|
<template slot-scope="scope"><el-input v-model="scope.row.purchaseLink" @paste.native="handleGridPaste($event, scope.$index, 8)" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="产品备注" min-width="180">
|
||||||
|
<template slot-scope="scope"><el-input v-model="scope.row.productRemark" @paste.native="handleGridPaste($event, scope.$index, 9)" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="申请备注" min-width="180">
|
||||||
|
<template slot-scope="scope"><el-input v-model="scope.row.remark" @paste.native="handleGridPaste($event, scope.$index, 10)" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="55" fixed="right" align="center">
|
||||||
|
<template slot-scope="scope"><el-button type="text" icon="el-icon-delete" title="删除行" @click="removeGridRow(scope.$index)" /></template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<div slot="footer" class="dialog-footer">
|
||||||
|
<el-button type="primary" :loading="gridSaving" @click="submitGrid">提交</el-button>
|
||||||
|
<el-button @click="gridOpen = false">取消</el-button>
|
||||||
|
</div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="采购状态" :visible.sync="statusOpen" width="480px" append-to-body>
|
||||||
|
<el-form ref="statusForm" :model="statusForm" :rules="statusRules" label-width="90px">
|
||||||
|
<el-form-item label="采购状态" prop="purchaseStatus">
|
||||||
|
<el-select v-model="statusForm.purchaseStatus">
|
||||||
|
<el-option v-for="item in manualStatusOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="进度说明" prop="progressRemark"><el-input v-model="statusForm.progressRemark" type="textarea" :rows="3" /></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<div slot="footer" class="dialog-footer"><el-button type="primary" @click="submitStatus">确定</el-button><el-button @click="statusOpen = false">取消</el-button></div>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog title="分批到货与验收" :visible.sync="arrivalOpen" width="1100px" custom-class="arrival-dialog" append-to-body>
|
||||||
|
<div v-if="arrivalRequest" class="arrival-header">
|
||||||
|
<strong>{{ arrivalRequest.materialName }}</strong>
|
||||||
|
<span>采购 {{ arrivalRequest.quantity }} {{ arrivalRequest.unit || '' }}</span>
|
||||||
|
<span>已验收 {{ arrivalRequest.acceptedQuantity || 0 }}</span>
|
||||||
|
<span>待验收 {{ arrivalRequest.pendingArrivalQuantity || 0 }}</span>
|
||||||
|
<span>可登记 {{ arrivalRequest.remainingQuantity || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<el-form v-if="arrivalRequest && canOperate(arrivalRequest)" ref="arrivalForm" :model="arrivalForm" :rules="arrivalRules" :inline="true" size="small" class="arrival-form">
|
||||||
|
<el-form-item label="本次到货" prop="arrivalQuantity"><el-input-number v-model="arrivalForm.arrivalQuantity" :min="0.001" :max="Number(arrivalRequest.remainingQuantity || 0)" :precision="3" :controls="false" /></el-form-item>
|
||||||
|
<el-form-item label="到货日期" prop="arrivalDate"><el-date-picker v-model="arrivalForm.arrivalDate" type="date" value-format="yyyy-MM-dd" /></el-form-item>
|
||||||
|
<el-form-item label="备注" prop="remark"><el-input v-model="arrivalForm.remark" /></el-form-item>
|
||||||
|
<el-form-item><el-button type="primary" :disabled="Number(arrivalRequest.remainingQuantity) <= 0" @click="submitArrival">登记到货</el-button></el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<el-table v-loading="arrivalLoading" :data="arrivalList" border class="arrival-table" max-height="440">
|
||||||
|
<el-table-column label="到货单号" prop="arrivalNo" min-width="175" />
|
||||||
|
<el-table-column label="到货日期" prop="arrivalDate" width="110" />
|
||||||
|
<el-table-column label="数量" prop="arrivalQuantity" width="100" align="right" />
|
||||||
|
<el-table-column label="验收状态" width="100" align="center"><template slot-scope="scope"><el-tag :type="acceptanceType(scope.row.acceptanceStatus)" size="mini">{{ acceptanceLabel(scope.row.acceptanceStatus) }}</el-tag></template></el-table-column>
|
||||||
|
<el-table-column label="到货备注" prop="remark" min-width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column label="验收说明" prop="acceptanceRemark" min-width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column label="撤销说明" prop="revokeRemark" min-width="130" show-overflow-tooltip />
|
||||||
|
<el-table-column label="验收人" prop="acceptedBy" width="100" />
|
||||||
|
<el-table-column label="操作" width="190" align="center" fixed="right">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<template v-if="scope.row.acceptanceStatus === 0 && arrivalRequest && canOperate(arrivalRequest)">
|
||||||
|
<el-button type="text" icon="el-icon-check" @click="handleAcceptance(scope.row, 1)">通过</el-button>
|
||||||
|
<el-button type="text" icon="el-icon-close" @click="handleAcceptance(scope.row, 2)">拒收</el-button>
|
||||||
|
<el-button type="text" icon="el-icon-delete" @click="handleDeleteArrival(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
<el-button v-else-if="arrivalRequest && canOperate(arrivalRequest)" type="text" icon="el-icon-refresh-left" @click="handleRevokeAcceptance(scope.row)">撤销验收</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<excel-import-dialog ref="purchaseImport" title="采购申请导入" action="/warehouse/purchase/importData" template-action="/warehouse/purchase/importTemplate" template-file-name="purchase_request_template" :show-update-support="false" @success="handleImportSuccess" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { listPurchase, getPurchase, addPurchase, tableAddPurchase, smartImportPurchase, updatePurchase, updatePurchaseStatus, delPurchase, listPurchaseArrival, addPurchaseArrival, acceptPurchaseArrival, revokePurchaseArrival, delPurchaseArrival, listPurchaseBatch, optionPurchaseBatch, filterOptionPurchaseBatch, addPurchaseBatch, updatePurchaseBatch, assignPurchaseBatch, delPurchaseBatch } from '@/api/warehouse/purchase'
|
||||||
|
import { optionselectProduct } from '@/api/warehouse/product'
|
||||||
|
import { optionselectUser } from '@/api/warehouse/user'
|
||||||
|
import ExcelImportDialog from '@/components/ExcelImportDialog'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PurchaseRequest',
|
||||||
|
components: { ExcelImportDialog },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
ids: [],
|
||||||
|
single: true,
|
||||||
|
multiple: true,
|
||||||
|
showSearch: true,
|
||||||
|
total: 0,
|
||||||
|
purchaseList: [],
|
||||||
|
productOptions: [],
|
||||||
|
open: false,
|
||||||
|
assignOpen: false,
|
||||||
|
batchOpen: false,
|
||||||
|
batchFormOpen: false,
|
||||||
|
gridOpen: false,
|
||||||
|
gridSaving: false,
|
||||||
|
gridRows: [],
|
||||||
|
smartImportOpen: false,
|
||||||
|
smartImportLoading: false,
|
||||||
|
smartFile: null,
|
||||||
|
smartFileList: [],
|
||||||
|
smartImportResultRows: null,
|
||||||
|
gridFields: ['productCode', 'materialName', 'materialBrand', 'category', 'materialParams', 'unit', 'quantity', 'expectedDate', 'purchaseLink', 'productRemark', 'remark'],
|
||||||
|
gridHeaderAliases: {
|
||||||
|
'关联物料编码': 'productCode', '物料编码': 'productCode',
|
||||||
|
'物料名称': 'materialName', '产品名称': 'materialName',
|
||||||
|
'品牌': 'materialBrand', '类别': 'category', '分类': 'category',
|
||||||
|
'参数规格': 'materialParams', '规格型号': 'materialParams',
|
||||||
|
'单位': 'unit', '需求数量': 'quantity', '数量': 'quantity',
|
||||||
|
'期望到货日期': 'expectedDate', '期望到货': 'expectedDate',
|
||||||
|
'采购链接': 'purchaseLink', '产品备注': 'productRemark', '申请备注': 'remark'
|
||||||
|
},
|
||||||
|
statusOpen: false,
|
||||||
|
statusForm: {},
|
||||||
|
arrivalOpen: false,
|
||||||
|
arrivalLoading: false,
|
||||||
|
arrivalRequest: null,
|
||||||
|
arrivalList: [],
|
||||||
|
arrivalForm: {},
|
||||||
|
title: '',
|
||||||
|
batchTitle: '',
|
||||||
|
batchList: [],
|
||||||
|
batchOptions: [],
|
||||||
|
batchFilterOptions: [],
|
||||||
|
userOptions: [],
|
||||||
|
batchLoading: false,
|
||||||
|
batchOptionLoading: false,
|
||||||
|
batchFilterLoading: false,
|
||||||
|
batchTotal: 0,
|
||||||
|
batchQuery: { pageNum: 1, pageSize: 10, batchNo: undefined, batchName: undefined, purchaseStatus: undefined },
|
||||||
|
statusOptions: [
|
||||||
|
{ value: 0, label: '待采购', type: 'info' },
|
||||||
|
{ value: 1, label: '已下单', type: '' },
|
||||||
|
{ value: 2, label: '采购中', type: 'warning' },
|
||||||
|
{ value: 3, label: '部分到货', type: 'warning' },
|
||||||
|
{ value: 4, label: '已到齐', type: 'success' },
|
||||||
|
{ value: 5, label: '已取消', type: 'danger' }
|
||||||
|
],
|
||||||
|
batchStatusOptions: [
|
||||||
|
{ value: 1, label: '规划中', type: 'info' },
|
||||||
|
{ value: 2, label: '执行中', type: 'warning' },
|
||||||
|
{ value: 3, label: '已结束', type: 'success' },
|
||||||
|
{ value: 4, label: '已归档', type: '' },
|
||||||
|
{ value: 5, label: '已取消', type: 'danger' }
|
||||||
|
],
|
||||||
|
queryParams: { pageNum: 1, pageSize: 10, materialName: undefined, purchaseStatus: undefined, purchaseBatchNo: undefined, applicantName: undefined },
|
||||||
|
form: {},
|
||||||
|
assignForm: {},
|
||||||
|
batchForm: {},
|
||||||
|
rules: {
|
||||||
|
materialName: [{ required: true, message: '物料名称不能为空', trigger: 'blur' }],
|
||||||
|
quantity: [{ required: true, message: '需求数量不能为空', trigger: 'change' }],
|
||||||
|
purchaseLink: [{ pattern: /^https?:\/\//i, message: '链接必须以 http:// 或 https:// 开头', trigger: 'blur' }]
|
||||||
|
},
|
||||||
|
assignRules: {
|
||||||
|
batchId: [{ required: true, message: '请选择目标采购批次', trigger: 'change' }]
|
||||||
|
},
|
||||||
|
batchRules: {
|
||||||
|
batchNo: [{ required: true, message: '批次编号不能为空', trigger: 'blur' }],
|
||||||
|
purchaseStatus: [{ required: true, message: '请选择批次状态', trigger: 'change' }]
|
||||||
|
},
|
||||||
|
statusRules: { purchaseStatus: [{ required: true, message: '请选择采购状态', trigger: 'change' }] },
|
||||||
|
arrivalRules: {
|
||||||
|
arrivalQuantity: [{ required: true, message: '到货数量不能为空', trigger: 'change' }],
|
||||||
|
arrivalDate: [{ required: true, message: '到货日期不能为空', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
canManageAll() {
|
||||||
|
const permissions = this.$store.getters.permissions || []
|
||||||
|
return permissions.includes('*:*:*') || permissions.includes('warehouse:purchase:manage')
|
||||||
|
},
|
||||||
|
manualStatusOptions() { return this.statusOptions.filter(item => [0, 1, 2, 5].includes(item.value)) },
|
||||||
|
productNameOptions() { return this.getDistinctOptions('productName') },
|
||||||
|
productCodeOptions() { return this.productOptions.filter(item => item.productCode) },
|
||||||
|
brandOptions() { return this.getDistinctOptions('brand') },
|
||||||
|
categoryOptions() { return this.getDistinctOptions('category') },
|
||||||
|
specOptions() { return this.getDistinctOptions('spec') },
|
||||||
|
unitOptions() { return this.getDistinctOptions('unit') }
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.getList()
|
||||||
|
this.getProductOptions()
|
||||||
|
this.getUserOptions()
|
||||||
|
this.searchBatchFilterOptions('')
|
||||||
|
if (this.canManageAll) this.getBatchOptions()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
userLabel(user) {
|
||||||
|
const account = user.userName || ''
|
||||||
|
return user.nickName && user.nickName !== account ? `${user.nickName}(${account})` : account
|
||||||
|
},
|
||||||
|
getUserOptions() {
|
||||||
|
optionselectUser().then(response => { this.userOptions = response.data || [] })
|
||||||
|
},
|
||||||
|
searchBatchFilterOptions(keyword) {
|
||||||
|
this.batchFilterLoading = true
|
||||||
|
filterOptionPurchaseBatch(keyword || '').then(response => {
|
||||||
|
this.batchFilterOptions = response.data || []
|
||||||
|
}).finally(() => { this.batchFilterLoading = false })
|
||||||
|
},
|
||||||
|
getList() {
|
||||||
|
this.loading = true
|
||||||
|
listPurchase(this.queryParams).then(response => {
|
||||||
|
this.purchaseList = response.rows
|
||||||
|
this.total = response.total
|
||||||
|
}).finally(() => { this.loading = false })
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
this.form = { id: undefined, productId: undefined, materialName: undefined, materialBrand: undefined, category: undefined, materialParams: undefined, productRemark: undefined, purchaseLink: undefined, quantity: 1, unit: undefined, expectedDate: undefined, remark: undefined }
|
||||||
|
this.resetForm('form')
|
||||||
|
},
|
||||||
|
cancel() { this.open = false; this.reset() },
|
||||||
|
handleQuery() { this.queryParams.pageNum = 1; this.getList() },
|
||||||
|
resetQuery() { this.resetForm('queryForm'); this.handleQuery() },
|
||||||
|
handleSelectionChange(selection) {
|
||||||
|
this.ids = selection.map(item => item.id)
|
||||||
|
this.single = selection.length !== 1
|
||||||
|
this.multiple = !selection.length
|
||||||
|
},
|
||||||
|
handleAdd() { this.reset(); this.title = '新增采购申请'; this.open = true },
|
||||||
|
handleUpdate(row) {
|
||||||
|
this.reset()
|
||||||
|
const id = row.id || this.ids[0]
|
||||||
|
getPurchase(id).then(response => {
|
||||||
|
this.form = response.data
|
||||||
|
this.title = '修改采购申请'
|
||||||
|
this.open = true
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
this.$refs.form.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
const updating = !!this.form.id
|
||||||
|
const action = updating ? updatePurchase(this.form) : addPurchase(this.form)
|
||||||
|
action.then(() => {
|
||||||
|
this.$modal.msgSuccess(updating ? '修改成功' : '申请已提交')
|
||||||
|
this.open = false
|
||||||
|
this.getList()
|
||||||
|
this.getProductOptions()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDelete(row) {
|
||||||
|
const ids = row.id || this.ids
|
||||||
|
this.$modal.confirm('是否确认删除选中的采购申请?').then(() => delPurchase(ids)).then(() => {
|
||||||
|
this.$modal.msgSuccess('删除成功')
|
||||||
|
this.getList()
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleStatus(row) {
|
||||||
|
this.statusForm = { id: row.id, purchaseStatus: row.purchaseStatus, progressRemark: row.progressRemark }
|
||||||
|
this.statusOpen = true
|
||||||
|
this.$nextTick(() => this.$refs.statusForm && this.$refs.statusForm.clearValidate())
|
||||||
|
},
|
||||||
|
submitStatus() {
|
||||||
|
this.$refs.statusForm.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
updatePurchaseStatus(this.statusForm.id, this.statusForm).then(() => {
|
||||||
|
this.$modal.msgSuccess('采购状态已更新')
|
||||||
|
this.statusOpen = false
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleArrival(row) {
|
||||||
|
this.arrivalOpen = true
|
||||||
|
this.arrivalRequest = { ...row }
|
||||||
|
this.resetArrivalForm()
|
||||||
|
this.refreshArrival(row.id)
|
||||||
|
},
|
||||||
|
refreshArrival(requestId) {
|
||||||
|
this.arrivalLoading = true
|
||||||
|
Promise.all([getPurchase(requestId), listPurchaseArrival(requestId)]).then(([requestResponse, arrivalResponse]) => {
|
||||||
|
this.arrivalRequest = requestResponse.data
|
||||||
|
this.arrivalList = arrivalResponse.data || []
|
||||||
|
}).finally(() => { this.arrivalLoading = false })
|
||||||
|
},
|
||||||
|
resetArrivalForm() {
|
||||||
|
this.arrivalForm = { purchaseRequestId: this.arrivalRequest && this.arrivalRequest.id, arrivalQuantity: undefined, arrivalDate: this.parseTime(new Date(), '{y}-{m}-{d}'), remark: undefined }
|
||||||
|
this.$nextTick(() => this.$refs.arrivalForm && this.$refs.arrivalForm.clearValidate())
|
||||||
|
},
|
||||||
|
submitArrival() {
|
||||||
|
this.arrivalForm.purchaseRequestId = this.arrivalRequest.id
|
||||||
|
this.$refs.arrivalForm.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
addPurchaseArrival(this.arrivalForm).then(() => {
|
||||||
|
this.$modal.msgSuccess('到货已登记,等待验收')
|
||||||
|
this.resetArrivalForm()
|
||||||
|
this.refreshArrival(this.arrivalRequest.id)
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleAcceptance(row, status) {
|
||||||
|
if (status === 1) {
|
||||||
|
this.$modal.confirm('确认本次到货数量和物料无误并通过验收?').then(() => acceptPurchaseArrival(row.id, { acceptanceStatus: 1 })).then(() => {
|
||||||
|
this.$modal.msgSuccess('验收通过,已生成待入库记录')
|
||||||
|
this.refreshArrival(this.arrivalRequest.id)
|
||||||
|
this.getList()
|
||||||
|
}).catch(() => {})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$prompt('请输入拒收原因', '拒收到货', { inputValidator: value => !!(value && value.trim()) || '拒收原因不能为空' }).then(({ value }) => {
|
||||||
|
return acceptPurchaseArrival(row.id, { acceptanceStatus: 2, acceptanceRemark: value })
|
||||||
|
}).then(() => {
|
||||||
|
this.$modal.msgSuccess('已拒收,本次数量可重新登记')
|
||||||
|
this.refreshArrival(this.arrivalRequest.id)
|
||||||
|
this.getList()
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleDeleteArrival(row) {
|
||||||
|
this.$modal.confirm('确认删除待验收到货记录“' + row.arrivalNo + '”?').then(() => delPurchaseArrival(row.id)).then(() => {
|
||||||
|
this.$modal.msgSuccess('到货记录已删除')
|
||||||
|
this.refreshArrival(this.arrivalRequest.id)
|
||||||
|
this.getList()
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleRevokeAcceptance(row) {
|
||||||
|
const action = row.acceptanceStatus === 1 ? '验收通过' : '拒收'
|
||||||
|
this.$prompt('请输入撤销原因', '撤销' + action, {
|
||||||
|
confirmButtonText: '确认撤销',
|
||||||
|
inputValidator: value => !!(value && value.trim()) || '撤销原因不能为空'
|
||||||
|
}).then(({ value }) => revokePurchaseArrival(row.id, { revokeRemark: value })).then(() => {
|
||||||
|
this.$modal.msgSuccess('验收已撤销,到货记录已恢复为待验收')
|
||||||
|
this.refreshArrival(this.arrivalRequest.id)
|
||||||
|
this.getList()
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleGridAdd() {
|
||||||
|
this.gridRows = Array.from({ length: 5 }, () => this.emptyGridRow())
|
||||||
|
this.gridOpen = true
|
||||||
|
},
|
||||||
|
handleSmartImport() {
|
||||||
|
this.resetSmartImport()
|
||||||
|
this.smartImportResultRows = null
|
||||||
|
this.smartImportOpen = true
|
||||||
|
},
|
||||||
|
handleSmartFileChange(file, fileList) {
|
||||||
|
this.smartFile = file.raw
|
||||||
|
this.smartFileList = fileList.slice(-1)
|
||||||
|
},
|
||||||
|
handleSmartFileRemove() {
|
||||||
|
this.smartFile = null
|
||||||
|
this.smartFileList = []
|
||||||
|
},
|
||||||
|
resetSmartImport() {
|
||||||
|
this.smartFile = null
|
||||||
|
this.smartFileList = []
|
||||||
|
if (this.$refs.smartUpload) this.$refs.smartUpload.clearFiles()
|
||||||
|
},
|
||||||
|
handleSmartImportClosed() {
|
||||||
|
this.resetSmartImport()
|
||||||
|
if (!this.smartImportResultRows) return
|
||||||
|
const count = this.smartImportResultRows.length
|
||||||
|
this.gridRows = this.smartImportResultRows
|
||||||
|
this.smartImportResultRows = null
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.gridOpen = true
|
||||||
|
this.$modal.msgSuccess(`分析完成,已载入 ${count} 条采购明细`)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitSmartImport() {
|
||||||
|
if (!this.smartFile) return this.$modal.msgWarning('请选择图片或Excel文件')
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', this.smartFile)
|
||||||
|
this.smartImportLoading = true
|
||||||
|
smartImportPurchase(formData).then(response => {
|
||||||
|
let result = response || {}
|
||||||
|
if (result.data && Array.isArray(result.data.items)) result = result.data
|
||||||
|
else if (result.data && result.data.data && Array.isArray(result.data.data.items)) result = result.data.data
|
||||||
|
const items = result.items || []
|
||||||
|
if (!Array.isArray(items) || !items.length) {
|
||||||
|
this.$modal.msgWarning((result.warnings || []).join(';') || '没有识别到可导入的采购明细')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.smartImportResultRows = items.map(item => Object.assign(this.emptyGridRow(), item || {}))
|
||||||
|
this.smartImportOpen = false
|
||||||
|
const warnings = result.warnings || []
|
||||||
|
if (warnings.length) this.$modal.msgWarning(warnings.join(';'))
|
||||||
|
}).catch(error => {
|
||||||
|
if (error && error.message && !/timeout|Network Error|status code/i.test(error.message)) {
|
||||||
|
this.$modal.msgError('分析结果处理失败:' + error.message)
|
||||||
|
}
|
||||||
|
}).finally(() => { this.smartImportLoading = false })
|
||||||
|
},
|
||||||
|
emptyGridRow() {
|
||||||
|
return { productId: undefined, productCode: undefined, materialName: undefined, materialBrand: undefined, category: undefined, materialParams: undefined, unit: undefined, quantity: 1, expectedDate: undefined, purchaseLink: undefined, productRemark: undefined, remark: undefined }
|
||||||
|
},
|
||||||
|
addGridRow() { this.gridRows.push(this.emptyGridRow()) },
|
||||||
|
removeGridRow(index) {
|
||||||
|
this.gridRows.splice(index, 1)
|
||||||
|
if (!this.gridRows.length) this.addGridRow()
|
||||||
|
},
|
||||||
|
removeEmptyGridRows() {
|
||||||
|
const rows = this.gridRows.filter(row => this.gridRowHasData(row))
|
||||||
|
this.gridRows = rows.length ? rows : [this.emptyGridRow()]
|
||||||
|
},
|
||||||
|
gridRowHasData(row) {
|
||||||
|
return this.gridFields.some(field => field !== 'quantity' && row[field]) || (row.quantity !== undefined && Number(row.quantity) !== 1)
|
||||||
|
},
|
||||||
|
handleGridPaste(event, startRow, startColumn) {
|
||||||
|
const text = event.clipboardData && event.clipboardData.getData('text/plain')
|
||||||
|
if (!text || (!text.includes('\t') && !/[\r\n]/.test(text))) return
|
||||||
|
event.preventDefault()
|
||||||
|
const rows = text.replace(/\r/g, '').split('\n')
|
||||||
|
if (rows.length && rows[rows.length - 1] === '') rows.pop()
|
||||||
|
const matrix = rows.map(row => row.split('\t'))
|
||||||
|
const headerFields = matrix.length
|
||||||
|
? matrix[0].map(value => this.gridHeaderAliases[this.normalizeGridHeader(value)])
|
||||||
|
: []
|
||||||
|
const hasHeader = headerFields.some(Boolean)
|
||||||
|
if (hasHeader) matrix.shift()
|
||||||
|
if (!matrix.length) return
|
||||||
|
while (this.gridRows.length < startRow + matrix.length) this.addGridRow()
|
||||||
|
matrix.forEach((cells, rowOffset) => {
|
||||||
|
const target = this.gridRows[startRow + rowOffset]
|
||||||
|
cells.forEach((value, columnOffset) => {
|
||||||
|
const field = hasHeader ? headerFields[columnOffset] : this.gridFields[startColumn + columnOffset]
|
||||||
|
if (!field) return
|
||||||
|
this.$set(target, field, field === 'quantity' ? this.parseGridNumber(value) : (field === 'expectedDate' ? this.normalizeGridDate(value) : value.trim()))
|
||||||
|
})
|
||||||
|
if (target.productCode) this.handleGridCodeChange(target)
|
||||||
|
else if (target.materialName) this.handleGridNameChange(target)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
normalizeGridHeader(value) {
|
||||||
|
return String(value || '').replace(/\s+/g, '').trim()
|
||||||
|
},
|
||||||
|
parseGridNumber(value) {
|
||||||
|
const number = Number(String(value).replace(/,/g, '').trim())
|
||||||
|
return Number.isFinite(number) ? number : value.trim()
|
||||||
|
},
|
||||||
|
normalizeGridDate(value) {
|
||||||
|
const text = String(value).trim()
|
||||||
|
if (/^\d+(\.\d+)?$/.test(text)) {
|
||||||
|
const date = new Date(Date.UTC(1899, 11, 30) + Number(text) * 86400000)
|
||||||
|
return this.parseTime(date, '{y}-{m}-{d}')
|
||||||
|
}
|
||||||
|
const matched = text.match(/^(\d{4})[\/.\-](\d{1,2})[\/.\-](\d{1,2})$/)
|
||||||
|
return matched ? matched[1] + '-' + matched[2].padStart(2, '0') + '-' + matched[3].padStart(2, '0') : text
|
||||||
|
},
|
||||||
|
handleGridCodeChange(row) {
|
||||||
|
const product = this.productOptions.find(item => this.sameText(item.productCode, row.productCode))
|
||||||
|
if (product) this.applyGridProduct(row, product, true)
|
||||||
|
else row.productId = undefined
|
||||||
|
},
|
||||||
|
handleGridNameChange(row) {
|
||||||
|
const products = this.productOptions.filter(item => this.sameText(item.productName, row.materialName))
|
||||||
|
if (products.length === 1) this.applyGridProduct(row, products[0], false)
|
||||||
|
else row.productId = undefined
|
||||||
|
},
|
||||||
|
applyGridProduct(row, product, overwrite) {
|
||||||
|
row.productId = product.id
|
||||||
|
row.productCode = product.productCode
|
||||||
|
const values = { materialName: product.productName, materialBrand: product.brand, category: product.category, materialParams: product.spec, unit: product.unit }
|
||||||
|
Object.keys(values).forEach(field => {
|
||||||
|
if (overwrite || !row[field]) this.$set(row, field, values[field])
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitGrid() {
|
||||||
|
const rows = this.gridRows.filter(row => this.gridRowHasData(row))
|
||||||
|
if (!rows.length) return this.$modal.msgError('请至少录入一行采购数据')
|
||||||
|
for (let index = 0; index < rows.length; index++) {
|
||||||
|
const row = rows[index]
|
||||||
|
if (!row.materialName) return this.$modal.msgError('第' + (index + 1) + '行物料名称不能为空')
|
||||||
|
if (!Number.isFinite(Number(row.quantity)) || Number(row.quantity) <= 0) return this.$modal.msgError('第' + (index + 1) + '行数量必须大于0')
|
||||||
|
if (row.purchaseLink && !/^https?:\/\//i.test(row.purchaseLink)) return this.$modal.msgError('第' + (index + 1) + '行采购链接格式不正确')
|
||||||
|
row.quantity = Number(row.quantity)
|
||||||
|
}
|
||||||
|
this.gridSaving = true
|
||||||
|
const payload = rows.map(row => {
|
||||||
|
const item = { productId: row.productId }
|
||||||
|
this.gridFields.forEach(field => { item[field] = row[field] })
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
tableAddPurchase(payload).then(response => {
|
||||||
|
this.$modal.msgSuccess('已提交 ' + response.data + ' 条采购申请')
|
||||||
|
this.gridOpen = false
|
||||||
|
this.getList()
|
||||||
|
this.getProductOptions()
|
||||||
|
}).finally(() => { this.gridSaving = false })
|
||||||
|
},
|
||||||
|
handleAssignBatch(row) {
|
||||||
|
const hasRow = row && row.id
|
||||||
|
this.assignForm = { requestIds: hasRow ? [row.id] : this.ids, batchId: undefined }
|
||||||
|
this.getBatchOptions()
|
||||||
|
this.assignOpen = true
|
||||||
|
this.$nextTick(() => this.$refs.assignForm && this.$refs.assignForm.clearValidate())
|
||||||
|
},
|
||||||
|
submitAssignBatch() {
|
||||||
|
this.$refs.assignForm.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
assignPurchaseBatch(this.assignForm).then(() => {
|
||||||
|
this.$modal.msgSuccess('采购批次已更新')
|
||||||
|
this.assignOpen = false
|
||||||
|
this.getList()
|
||||||
|
this.getBatchList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getBatchList() {
|
||||||
|
this.batchLoading = true
|
||||||
|
listPurchaseBatch(this.batchQuery).then(response => {
|
||||||
|
this.batchList = response.rows || []
|
||||||
|
this.batchTotal = response.total || 0
|
||||||
|
}).finally(() => { this.batchLoading = false })
|
||||||
|
},
|
||||||
|
getBatchOptions(keyword) {
|
||||||
|
this.batchOptionLoading = true
|
||||||
|
optionPurchaseBatch(keyword || '').then(response => { this.batchOptions = response.data || [] }).finally(() => { this.batchOptionLoading = false })
|
||||||
|
},
|
||||||
|
searchBatchOptions(keyword) {
|
||||||
|
this.getBatchOptions(keyword)
|
||||||
|
},
|
||||||
|
handleBatchManage() { this.batchQuery.pageNum = 1; this.getBatchList(); this.batchOpen = true },
|
||||||
|
handleBatchQuery() { this.batchQuery.pageNum = 1; this.getBatchList() },
|
||||||
|
resetBatchQuery() { this.resetForm('batchQueryForm'); this.handleBatchQuery() },
|
||||||
|
handleAddBatch() {
|
||||||
|
this.batchForm = { id: undefined, batchNo: undefined, batchName: undefined, purchaseStatus: 1, progressRemark: undefined }
|
||||||
|
this.batchTitle = '新增采购批次'
|
||||||
|
this.batchFormOpen = true
|
||||||
|
this.$nextTick(() => this.$refs.batchForm && this.$refs.batchForm.clearValidate())
|
||||||
|
},
|
||||||
|
handleEditBatch(row) {
|
||||||
|
this.batchForm = { ...row }
|
||||||
|
this.batchTitle = '修改采购批次'
|
||||||
|
this.batchFormOpen = true
|
||||||
|
this.$nextTick(() => this.$refs.batchForm && this.$refs.batchForm.clearValidate())
|
||||||
|
},
|
||||||
|
submitBatch() {
|
||||||
|
this.$refs.batchForm.validate(valid => {
|
||||||
|
if (!valid) return
|
||||||
|
const updating = !!this.batchForm.id
|
||||||
|
const action = updating ? updatePurchaseBatch(this.batchForm) : addPurchaseBatch(this.batchForm)
|
||||||
|
action.then(() => {
|
||||||
|
this.$modal.msgSuccess(updating ? '批次已更新' : '批次已创建')
|
||||||
|
this.batchFormOpen = false
|
||||||
|
this.getBatchList()
|
||||||
|
this.getBatchOptions()
|
||||||
|
this.getList()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
handleDeleteBatch(row) {
|
||||||
|
this.$modal.confirm('是否删除采购批次“' + row.batchNo + '”?有关联申请的批次不能删除。').then(() => delPurchaseBatch(row.id)).then(() => {
|
||||||
|
this.$modal.msgSuccess('批次已删除')
|
||||||
|
this.getBatchList()
|
||||||
|
}).catch(() => {})
|
||||||
|
},
|
||||||
|
handleImport() { this.$refs.purchaseImport.open() },
|
||||||
|
handleImportSuccess() { this.getList(); this.getProductOptions() },
|
||||||
|
handleExport() { this.download('warehouse/purchase/export', { ...this.queryParams }, `purchase_request_${new Date().getTime()}.xlsx`) },
|
||||||
|
getProductOptions() {
|
||||||
|
optionselectProduct().then(response => { this.productOptions = response.data || [] })
|
||||||
|
},
|
||||||
|
getDistinctOptions(field) {
|
||||||
|
const values = this.productOptions.map(item => item[field]).filter(Boolean)
|
||||||
|
return Array.from(new Set(values))
|
||||||
|
},
|
||||||
|
canOperate(row) {
|
||||||
|
return this.canManageAll || Number(row.applicantId) === Number(this.$store.getters.id)
|
||||||
|
},
|
||||||
|
handleExistingProductChange(id) {
|
||||||
|
if (!id) return
|
||||||
|
const product = this.productOptions.find(item => item.id === id)
|
||||||
|
if (!product) return
|
||||||
|
this.applyProduct(product)
|
||||||
|
},
|
||||||
|
handleMaterialNameChange(value) {
|
||||||
|
this.form.productId = undefined
|
||||||
|
const products = this.productOptions.filter(item => this.sameText(item.productName, value))
|
||||||
|
if (products.length === 1) {
|
||||||
|
this.applyProduct(products[0])
|
||||||
|
} else {
|
||||||
|
this.syncProductId()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
applyProduct(product) {
|
||||||
|
this.form.productId = product.id
|
||||||
|
this.form.productCode = product.productCode
|
||||||
|
this.form.materialName = product.productName
|
||||||
|
this.form.materialBrand = product.brand
|
||||||
|
this.form.category = product.category
|
||||||
|
this.form.materialParams = product.spec
|
||||||
|
this.form.unit = product.unit
|
||||||
|
this.form.productRemark = product.remark
|
||||||
|
},
|
||||||
|
syncProductId() {
|
||||||
|
const product = this.productOptions.find(item => this.sameText(item.productName, this.form.materialName) && this.sameText(item.brand, this.form.materialBrand) && this.sameText(item.category, this.form.category) && this.sameText(item.spec, this.form.materialParams))
|
||||||
|
this.form.productId = product ? product.id : undefined
|
||||||
|
this.form.productCode = product ? product.productCode : undefined
|
||||||
|
},
|
||||||
|
sameText(left, right) { return (left || '').trim().toLowerCase() === (right || '').trim().toLowerCase() },
|
||||||
|
productLabel(item) { return [item.productCode, item.productName, item.brand, item.category, item.spec, item.unit].filter(Boolean).join(' / ') },
|
||||||
|
batchLabel(item) { return [item.batchNo, item.batchName, this.batchStatusLabel(item.purchaseStatus)].filter(Boolean).join(' / ') },
|
||||||
|
batchStatusLabel(value) { const item = this.batchStatusOptions.find(option => option.value === value); return item ? item.label : '未知' },
|
||||||
|
batchStatusType(value) { const item = this.batchStatusOptions.find(option => option.value === value); return item ? item.type : 'info' },
|
||||||
|
acceptanceLabel(value) { return ['待验收', '已通过', '已拒收'][value] || '未知' },
|
||||||
|
acceptanceType(value) { return ['warning', 'success', 'danger'][value] || 'info' },
|
||||||
|
statusLabel(value) { const item = this.statusOptions.find(option => option.value === value); return item ? item.label : '未知' },
|
||||||
|
statusType(value) { const item = this.statusOptions.find(option => option.value === value); return item ? item.type : 'info' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-select,
|
||||||
|
.el-date-editor,
|
||||||
|
.el-input-number {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .purchase-dialog {
|
||||||
|
max-width: calc(100vw - 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrival-summary {
|
||||||
|
color: #e6a23c;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .purchase-smart-dialog {
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .purchase-smart-dialog .el-upload,
|
||||||
|
::v-deep .purchase-smart-dialog .el-upload-dragger {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smart-tip {
|
||||||
|
color: #e6a23c;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrival-header {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 20px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrival-form {
|
||||||
|
padding: 12px 0 2px;
|
||||||
|
border-top: 1px solid #ebeef5;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .purchase-grid-dialog {
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .arrival-dialog {
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .purchase-grid .cell {
|
||||||
|
padding: 3px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .purchase-grid .el-input__inner {
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -32,8 +32,11 @@ drop table if exists in_record;
|
|||||||
create table in_record (
|
create table in_record (
|
||||||
id bigint(20) not null auto_increment comment '主键',
|
id bigint(20) not null auto_increment comment '主键',
|
||||||
in_no varchar(64) not null comment '入库单号',
|
in_no varchar(64) not null comment '入库单号',
|
||||||
area_id bigint(20) not null comment '库区id',
|
area_id bigint(20) default null comment '库区id,待入库时可为空',
|
||||||
product_id bigint(20) not null comment '产品id',
|
product_id bigint(20) not null comment '产品id',
|
||||||
|
purchase_request_id bigint(20) default null comment '采购申请ID',
|
||||||
|
arrival_id bigint(20) default null comment '采购到货ID',
|
||||||
|
source_no varchar(64) default null comment '来源单号',
|
||||||
in_type tinyint(1) not null comment '入库类型',
|
in_type tinyint(1) not null comment '入库类型',
|
||||||
in_num decimal(16,3) not null comment '入库数量',
|
in_num decimal(16,3) not null comment '入库数量',
|
||||||
in_status tinyint(1) not null default 0 comment '入库状态',
|
in_status tinyint(1) not null default 0 comment '入库状态',
|
||||||
@ -45,6 +48,8 @@ create table in_record (
|
|||||||
update_time datetime default null comment '更新时间',
|
update_time datetime default null comment '更新时间',
|
||||||
primary key (id),
|
primary key (id),
|
||||||
unique key uk_in_record_no (in_no),
|
unique key uk_in_record_no (in_no),
|
||||||
|
key idx_in_record_purchase_request (purchase_request_id),
|
||||||
|
unique key uk_in_record_arrival (arrival_id),
|
||||||
key idx_in_record_area_product (area_id, product_id)
|
key idx_in_record_area_product (area_id, product_id)
|
||||||
) engine=innodb auto_increment=1 comment='入库记录表';
|
) engine=innodb auto_increment=1 comment='入库记录表';
|
||||||
|
|
||||||
|
|||||||
279
sql/wms_purchase_request.sql
Normal file
279
sql/wms_purchase_request.sql
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
-- 采购申请模块。执行一次后,在角色管理中给员工分配采购申请菜单;
|
||||||
|
-- “采购管理”按钮权限仅分配给采购管理员。
|
||||||
|
|
||||||
|
create table if not exists purchase_batch (
|
||||||
|
id bigint(20) not null auto_increment comment '主键',
|
||||||
|
batch_no varchar(64) not null comment '采购批次编号',
|
||||||
|
batch_name varchar(200) default null comment '采购批次名称',
|
||||||
|
purchase_status tinyint(1) not null default 1 comment '批次状态:1规划中 2执行中 3已结束 4已归档 5已取消',
|
||||||
|
progress_remark varchar(500) default null comment '进度说明',
|
||||||
|
create_by varchar(64) default null comment '创建人',
|
||||||
|
create_time datetime default null comment '创建时间',
|
||||||
|
update_by varchar(64) default null comment '更新人',
|
||||||
|
update_time datetime default null comment '更新时间',
|
||||||
|
primary key (id),
|
||||||
|
unique key uk_purchase_batch_no (batch_no)
|
||||||
|
) engine=innodb default charset=utf8mb4 comment='采购批次表';
|
||||||
|
|
||||||
|
alter table purchase_batch modify column purchase_status tinyint(1) not null default 1
|
||||||
|
comment '批次状态:1规划中 2执行中 3已结束 4已归档 5已取消';
|
||||||
|
|
||||||
|
create table if not exists purchase_request (
|
||||||
|
id bigint(20) not null auto_increment comment '主键',
|
||||||
|
request_no varchar(64) not null comment '申请单号',
|
||||||
|
product_id bigint(20) default null comment '关联库房物料ID',
|
||||||
|
purchase_batch_id bigint(20) default null comment '采购批次ID',
|
||||||
|
material_name varchar(200) not null comment '物料名称',
|
||||||
|
material_brand varchar(100) default null comment '品牌',
|
||||||
|
category varchar(100) default null comment '类别',
|
||||||
|
material_params varchar(500) default null comment '参数规格',
|
||||||
|
purchase_link varchar(1000) default null comment '采购链接',
|
||||||
|
quantity decimal(16,3) not null default 1.000 comment '需求数量',
|
||||||
|
unit varchar(32) default null comment '单位',
|
||||||
|
expected_date date default null comment '期望到货日期',
|
||||||
|
applicant_id bigint(20) not null comment '申请人用户ID',
|
||||||
|
applicant_name varchar(64) not null comment '申请人账号',
|
||||||
|
purchase_status tinyint(1) not null default 0 comment '状态:0待采购 1已下单 2采购中 3部分到货 4已到齐 5已取消',
|
||||||
|
purchase_batch_no varchar(64) default null comment '采购批次',
|
||||||
|
progress_remark varchar(500) default null comment '进度说明',
|
||||||
|
remark varchar(500) default null comment '申请备注',
|
||||||
|
create_by varchar(64) default null comment '创建人',
|
||||||
|
create_time datetime default null comment '创建时间',
|
||||||
|
update_by varchar(64) default null comment '更新人',
|
||||||
|
update_time datetime default null comment '更新时间',
|
||||||
|
primary key (id),
|
||||||
|
unique key uk_purchase_request_no (request_no),
|
||||||
|
key idx_purchase_request_product (product_id),
|
||||||
|
key idx_purchase_request_batch_id (purchase_batch_id),
|
||||||
|
key idx_purchase_request_applicant (applicant_id),
|
||||||
|
key idx_purchase_request_status (purchase_status),
|
||||||
|
key idx_purchase_request_batch (purchase_batch_no)
|
||||||
|
|
||||||
|
|
||||||
|
) engine=innodb default charset=utf8mb4 comment='采购申请表';
|
||||||
|
|
||||||
|
create table if not exists purchase_arrival (
|
||||||
|
id bigint(20) not null auto_increment comment '主键',
|
||||||
|
arrival_no varchar(64) not null comment '到货单号',
|
||||||
|
purchase_request_id bigint(20) not null comment '采购申请ID',
|
||||||
|
arrival_quantity decimal(16,3) not null comment '本次到货数量',
|
||||||
|
arrival_date date not null comment '到货日期',
|
||||||
|
acceptance_status tinyint(1) not null default 0 comment '验收状态:0待验收 1验收通过 2验收拒绝',
|
||||||
|
acceptance_remark varchar(500) default null comment '验收说明',
|
||||||
|
accepted_by varchar(64) default null comment '验收人',
|
||||||
|
accepted_time datetime default null comment '验收时间',
|
||||||
|
pre_accept_purchase_status tinyint(1) default null comment '验收前采购状态',
|
||||||
|
revoked_by varchar(64) default null comment '最近撤销人',
|
||||||
|
revoked_time datetime default null comment '最近撤销时间',
|
||||||
|
revoke_remark varchar(500) default null comment '最近撤销原因',
|
||||||
|
remark varchar(500) default null comment '到货备注',
|
||||||
|
create_by varchar(64) default null comment '创建人',
|
||||||
|
create_time datetime default null comment '创建时间',
|
||||||
|
update_by varchar(64) default null comment '更新人',
|
||||||
|
update_time datetime default null comment '更新时间',
|
||||||
|
primary key (id),
|
||||||
|
unique key uk_purchase_arrival_no (arrival_no),
|
||||||
|
key idx_purchase_arrival_request (purchase_request_id),
|
||||||
|
key idx_purchase_arrival_status (acceptance_status)
|
||||||
|
) engine=innodb default charset=utf8mb4 comment='采购分批到货及验收记录';
|
||||||
|
|
||||||
|
-- 兼容已执行过早期脚本的数据库。
|
||||||
|
set @purchase_brand_column = (
|
||||||
|
select count(*) from information_schema.columns
|
||||||
|
where table_schema = database() and table_name = 'purchase_request' and column_name = 'material_brand'
|
||||||
|
);
|
||||||
|
set @purchase_brand_sql = if(
|
||||||
|
@purchase_brand_column = 0,
|
||||||
|
'alter table purchase_request add column material_brand varchar(100) default null comment ''品牌'' after material_name',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare purchase_brand_stmt from @purchase_brand_sql;
|
||||||
|
execute purchase_brand_stmt;
|
||||||
|
deallocate prepare purchase_brand_stmt;
|
||||||
|
|
||||||
|
set @purchase_batch_id_column = (
|
||||||
|
select count(*) from information_schema.columns
|
||||||
|
where table_schema = database() and table_name = 'purchase_request' and column_name = 'purchase_batch_id'
|
||||||
|
);
|
||||||
|
set @purchase_batch_id_sql = if(
|
||||||
|
@purchase_batch_id_column = 0,
|
||||||
|
'alter table purchase_request add column purchase_batch_id bigint(20) default null comment ''采购批次ID'' after product_id, add key idx_purchase_request_batch_id (purchase_batch_id)',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare purchase_batch_id_stmt from @purchase_batch_id_sql;
|
||||||
|
execute purchase_batch_id_stmt;
|
||||||
|
deallocate prepare purchase_batch_id_stmt;
|
||||||
|
|
||||||
|
set @arrival_revoke_columns = (
|
||||||
|
select count(*) from information_schema.columns
|
||||||
|
where table_schema = database() and table_name = 'purchase_arrival' and column_name = 'pre_accept_purchase_status'
|
||||||
|
);
|
||||||
|
set @arrival_revoke_sql = if(
|
||||||
|
@arrival_revoke_columns = 0,
|
||||||
|
'alter table purchase_arrival add column pre_accept_purchase_status tinyint(1) default null comment ''验收前采购状态'' after accepted_time, add column revoked_by varchar(64) default null comment ''最近撤销人'' after pre_accept_purchase_status, add column revoked_time datetime default null comment ''最近撤销时间'' after revoked_by, add column revoke_remark varchar(500) default null comment ''最近撤销原因'' after revoked_time',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare arrival_revoke_stmt from @arrival_revoke_sql;
|
||||||
|
execute arrival_revoke_stmt;
|
||||||
|
deallocate prepare arrival_revoke_stmt;
|
||||||
|
|
||||||
|
alter table purchase_request modify column purchase_status tinyint(1) not null default 0
|
||||||
|
comment '状态:0待采购 1已下单 2采购中 3部分到货 4已到齐 5已取消';
|
||||||
|
|
||||||
|
-- 把旧版本中写在申请行上的批次编号迁移为批次主数据。
|
||||||
|
insert ignore into purchase_batch(batch_no, batch_name, purchase_status, progress_remark, create_by, create_time)
|
||||||
|
select purchase_batch_no, purchase_batch_no, greatest(max(purchase_status), 1), max(progress_remark), 'migration', sysdate()
|
||||||
|
from purchase_request
|
||||||
|
where purchase_batch_no is not null and purchase_batch_no != ''
|
||||||
|
group by purchase_batch_no;
|
||||||
|
|
||||||
|
update purchase_request pr
|
||||||
|
join purchase_batch pb on pb.batch_no = pr.purchase_batch_no
|
||||||
|
set pr.purchase_batch_id = pb.id,
|
||||||
|
pr.purchase_status = pb.purchase_status,
|
||||||
|
pr.progress_remark = pb.progress_remark
|
||||||
|
where pr.purchase_batch_id is null;
|
||||||
|
|
||||||
|
-- 采购完成后先生成待入库记录,库区和入库人由仓库人员补充后再确认入库。
|
||||||
|
alter table in_record modify column area_id bigint(20) default null comment '库区id,待入库时可为空';
|
||||||
|
|
||||||
|
set @in_purchase_request_column = (
|
||||||
|
select count(*) from information_schema.columns
|
||||||
|
where table_schema = database() and table_name = 'in_record' and column_name = 'purchase_request_id'
|
||||||
|
);
|
||||||
|
set @in_purchase_request_sql = if(
|
||||||
|
@in_purchase_request_column = 0,
|
||||||
|
'alter table in_record add column purchase_request_id bigint(20) default null comment ''采购申请ID'' after product_id',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare in_purchase_request_stmt from @in_purchase_request_sql;
|
||||||
|
execute in_purchase_request_stmt;
|
||||||
|
deallocate prepare in_purchase_request_stmt;
|
||||||
|
|
||||||
|
set @in_source_no_column = (
|
||||||
|
select count(*) from information_schema.columns
|
||||||
|
where table_schema = database() and table_name = 'in_record' and column_name = 'source_no'
|
||||||
|
);
|
||||||
|
set @in_source_no_sql = if(
|
||||||
|
@in_source_no_column = 0,
|
||||||
|
'alter table in_record add column source_no varchar(64) default null comment ''来源单号'' after purchase_request_id',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare in_source_no_stmt from @in_source_no_sql;
|
||||||
|
execute in_source_no_stmt;
|
||||||
|
deallocate prepare in_source_no_stmt;
|
||||||
|
|
||||||
|
set @in_arrival_column = (
|
||||||
|
select count(*) from information_schema.columns
|
||||||
|
where table_schema = database() and table_name = 'in_record' and column_name = 'arrival_id'
|
||||||
|
);
|
||||||
|
set @in_arrival_sql = if(
|
||||||
|
@in_arrival_column = 0,
|
||||||
|
'alter table in_record add column arrival_id bigint(20) default null comment ''采购到货ID'' after purchase_request_id',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare in_arrival_stmt from @in_arrival_sql;
|
||||||
|
execute in_arrival_stmt;
|
||||||
|
deallocate prepare in_arrival_stmt;
|
||||||
|
|
||||||
|
set @in_purchase_request_unique_index = (
|
||||||
|
select count(*) from information_schema.statistics
|
||||||
|
where table_schema = database() and table_name = 'in_record'
|
||||||
|
and index_name = 'uk_in_record_purchase_request' and non_unique = 0
|
||||||
|
);
|
||||||
|
set @drop_in_purchase_request_unique_sql = if(
|
||||||
|
@in_purchase_request_unique_index > 0,
|
||||||
|
'alter table in_record drop index uk_in_record_purchase_request',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare drop_in_purchase_request_unique_stmt from @drop_in_purchase_request_unique_sql;
|
||||||
|
execute drop_in_purchase_request_unique_stmt;
|
||||||
|
deallocate prepare drop_in_purchase_request_unique_stmt;
|
||||||
|
|
||||||
|
set @in_purchase_request_index = (
|
||||||
|
select count(*) from information_schema.statistics
|
||||||
|
where table_schema = database() and table_name = 'in_record' and index_name = 'idx_in_record_purchase_request'
|
||||||
|
);
|
||||||
|
set @in_purchase_request_index_sql = if(
|
||||||
|
@in_purchase_request_index = 0,
|
||||||
|
'alter table in_record add key idx_in_record_purchase_request (purchase_request_id)',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare in_purchase_request_index_stmt from @in_purchase_request_index_sql;
|
||||||
|
execute in_purchase_request_index_stmt;
|
||||||
|
deallocate prepare in_purchase_request_index_stmt;
|
||||||
|
|
||||||
|
set @in_arrival_index = (
|
||||||
|
select count(*) from information_schema.statistics
|
||||||
|
where table_schema = database() and table_name = 'in_record' and index_name = 'uk_in_record_arrival'
|
||||||
|
);
|
||||||
|
set @in_arrival_index_sql = if(
|
||||||
|
@in_arrival_index = 0,
|
||||||
|
'alter table in_record add unique key uk_in_record_arrival (arrival_id)',
|
||||||
|
'select 1'
|
||||||
|
);
|
||||||
|
prepare in_arrival_index_stmt from @in_arrival_index_sql;
|
||||||
|
execute in_arrival_index_stmt;
|
||||||
|
deallocate prepare in_arrival_index_stmt;
|
||||||
|
|
||||||
|
-- 旧版本按批次完成自动生成、但尚未真实入库的记录没有经过验收,升级时移除。
|
||||||
|
delete from in_record
|
||||||
|
where arrival_id is null and purchase_request_id is not null
|
||||||
|
and in_status = 0 and in_no like 'PIN%';
|
||||||
|
|
||||||
|
-- 菜单及按钮权限
|
||||||
|
delete from sys_menu where menu_id in (2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042);
|
||||||
|
insert into sys_menu values('2035', '采购申请', '0', '6', 'purchase', 'warehouse/purchase/index', '', '', 1, 0, 'C', '0', '0', 'warehouse:purchase:list', 'shopping', 'admin', sysdate(), '', null, '与库存管理同级的采购申请菜单');
|
||||||
|
insert into sys_menu values('2036', '采购查询', '2035', '1', '#', '', '', '', 1, 0, 'F', '0', '0', 'warehouse:purchase:query', '#', 'admin', sysdate(), '', null, '');
|
||||||
|
insert into sys_menu values('2037', '采购申请新增', '2035', '2', '#', '', '', '', 1, 0, 'F', '0', '0', 'warehouse:purchase:add', '#', 'admin', sysdate(), '', null, '');
|
||||||
|
insert into sys_menu values('2038', '采购申请修改', '2035', '3', '#', '', '', '', 1, 0, 'F', '0', '0', 'warehouse:purchase:edit', '#', 'admin', sysdate(), '', null, '');
|
||||||
|
insert into sys_menu values('2039', '采购申请删除', '2035', '4', '#', '', '', '', 1, 0, 'F', '0', '0', 'warehouse:purchase:remove', '#', 'admin', sysdate(), '', null, '');
|
||||||
|
insert into sys_menu values('2040', '采购申请导入', '2035', '5', '#', '', '', '', 1, 0, 'F', '0', '0', 'warehouse:purchase:import', '#', 'admin', sysdate(), '', null, '');
|
||||||
|
insert into sys_menu values('2041', '采购申请导出', '2035', '6', '#', '', '', '', 1, 0, 'F', '0', '0', 'warehouse:purchase:export', '#', 'admin', sysdate(), '', null, '');
|
||||||
|
insert into sys_menu values('2042', '采购管理', '2035', '7', '#', '', '', '', 1, 0, 'F', '0', '0', 'warehouse:purchase:manage', '#', 'admin', sysdate(), '', null, '仅管理员分配,可查看全部申请并修改采购进度');
|
||||||
|
|
||||||
|
-- 独立采购管理员角色。该角色可分配给普通用户,不使用受保护的 admin 超级管理员角色。
|
||||||
|
insert into sys_role(role_name, role_key, role_sort, data_scope, menu_check_strictly,
|
||||||
|
dept_check_strictly, status, del_flag, create_by, create_time, remark)
|
||||||
|
select '采购管理员', 'purchase_manager', 3, '1', 1, 1, '0', '0', 'admin', sysdate(),
|
||||||
|
'维护全部采购申请、采购批次和采购进度'
|
||||||
|
where not exists (
|
||||||
|
select 1 from sys_role where role_key = 'purchase_manager' and del_flag = '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
set @purchase_manager_role_id = (
|
||||||
|
select role_id from sys_role
|
||||||
|
where role_key = 'purchase_manager' and del_flag = '0'
|
||||||
|
order by role_id limit 1
|
||||||
|
);
|
||||||
|
|
||||||
|
insert ignore into sys_role_menu(role_id, menu_id)
|
||||||
|
select @purchase_manager_role_id, menu_id
|
||||||
|
from sys_menu
|
||||||
|
where menu_id in (2035, 2036, 2037, 2038, 2039, 2040, 2041, 2042);
|
||||||
|
|
||||||
|
delete from sys_role_menu
|
||||||
|
where role_id = @purchase_manager_role_id and menu_id = 2000;
|
||||||
|
|
||||||
|
-- 普通采购申请人:可查看、导出全部申请,但服务端只允许修改和删除本人数据。
|
||||||
|
insert into sys_role(role_name, role_key, role_sort, data_scope, menu_check_strictly,
|
||||||
|
dept_check_strictly, status, del_flag, create_by, create_time, remark)
|
||||||
|
select '采购申请人', 'purchase_user', 4, '1', 1, 1, '0', '0', 'admin', sysdate(),
|
||||||
|
'提交采购申请;查看和导出全部申请;修改、删除本人申请'
|
||||||
|
where not exists (
|
||||||
|
select 1 from sys_role where role_key = 'purchase_user' and del_flag = '0'
|
||||||
|
);
|
||||||
|
|
||||||
|
set @purchase_user_role_id = (
|
||||||
|
select role_id from sys_role
|
||||||
|
where role_key = 'purchase_user' and del_flag = '0'
|
||||||
|
order by role_id limit 1
|
||||||
|
);
|
||||||
|
|
||||||
|
insert ignore into sys_role_menu(role_id, menu_id)
|
||||||
|
select @purchase_user_role_id, menu_id
|
||||||
|
from sys_menu
|
||||||
|
where menu_id in (2035, 2036, 2037, 2038, 2039, 2040, 2041);
|
||||||
|
|
||||||
|
delete from sys_role_menu
|
||||||
|
where role_id = @purchase_user_role_id and menu_id = 2000;
|
||||||
Loading…
Reference in New Issue
Block a user