zlwu 11 месяцев назад
Родитель
Сommit
dab9e87caf

+ 5 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/CrmQuote.java

@@ -40,6 +40,11 @@ public class CrmQuote extends TenantEntity {
      */
     private String quoteTitle;
 
+    /**
+     * 报价单类型(sales-销售/purchase-采购)
+     */
+    private String quoteType;
+
     /**
      * 客户ID
      */

+ 5 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/bo/CrmCustomerBo.java

@@ -82,6 +82,7 @@ public class CrmCustomerBo extends BaseEntity {
     /**
      * 客户来源
      */
+    @NotBlank(message = "客户来源不能为空", groups = { AddGroup.class, EditGroup.class })
     private String source;
 
     /**
@@ -92,6 +93,7 @@ public class CrmCustomerBo extends BaseEntity {
     /**
      * 所在园区
      */
+    @NotBlank(message = "所在园区不能为空", groups = { AddGroup.class, EditGroup.class })
     private String parkArea;
 
     /**
@@ -102,16 +104,19 @@ public class CrmCustomerBo extends BaseEntity {
     /**
      * 省
      */
+    @NotBlank(message = "省份不能为空", groups = { AddGroup.class, EditGroup.class })
     private String province;
 
     /**
      * 市
      */
+    @NotBlank(message = "城市不能为空", groups = { AddGroup.class, EditGroup.class })
     private String city;
 
     /**
      * 区
      */
+    @NotBlank(message = "区县不能为空", groups = { AddGroup.class, EditGroup.class })
     private String district;
 
     /**

+ 7 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/bo/CrmQuoteBo.java

@@ -40,6 +40,13 @@ public class CrmQuoteBo extends BaseEntity {
      */
     private String quoteTitle;
 
+    /**
+     * 报价单类型(sales-销售/purchase-采购)
+     */
+    @NotBlank(message = "报价单类型不能为空", groups = { AddGroup.class, EditGroup.class })
+    @Pattern(regexp = "sales|purchase", message = "报价单类型只能为销售或采购", groups = { AddGroup.class, EditGroup.class })
+    private String quoteType;
+
     /**
      * 客户ID
      */

+ 6 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/domain/vo/CrmQuoteVo.java

@@ -50,6 +50,12 @@ public class CrmQuoteVo implements Serializable {
     @ExcelProperty(value = "报价单标题")
     private String quoteTitle;
 
+    /**
+     * 报价单类型(sales-销售/purchase-采购)
+     */
+    @ExcelProperty(value = "报价单类型")
+    private String quoteType;
+
     /**
      * 客户ID
      */

+ 9 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/java/org/dromara/crm/service/impl/CrmQuoteServiceImpl.java

@@ -89,6 +89,7 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
         lqw.eq(bo.getCustomerId() != null, CrmQuote::getCustomerId, bo.getCustomerId());
         lqw.eq(bo.getOpportunityId() != null, CrmQuote::getOpportunityId, bo.getOpportunityId());
         lqw.eq(StringUtils.isNotBlank(bo.getStatus()), CrmQuote::getStatus, bo.getStatus());
+        lqw.eq(StringUtils.isNotBlank(bo.getQuoteType()), CrmQuote::getQuoteType, bo.getQuoteType());
         lqw.eq(bo.getOwnerUserId() != null, CrmQuote::getOwnerUserId, bo.getOwnerUserId());
         lqw.eq(bo.getIsLatest() != null, CrmQuote::getIsLatest, bo.getIsLatest());
         lqw.ge(bo.getParams().get("minAmount") != null, CrmQuote::getFinalAmount, bo.getParams().get("minAmount"));
@@ -271,6 +272,9 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
         if (entity.getStatus() == null) {
             entity.setStatus("draft");
         }
+        if (StringUtils.isBlank(entity.getQuoteType())) {
+            entity.setQuoteType("sales");
+        }
         if (entity.getValidityDays() == null) {
             entity.setValidityDays(30);
         }
@@ -341,6 +345,10 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
             throw new ServiceException("只有草稿状态的报价单才能修改");
         }
 
+        if (StringUtils.isBlank(entity.getQuoteType())) {
+            entity.setQuoteType(StringUtils.isNotBlank(oldEntity.getQuoteType()) ? oldEntity.getQuoteType() : "sales");
+        }
+
         // 填充联系人手机和邮箱
         if (entity.getContactId() != null) {
             try {
@@ -797,6 +805,7 @@ public class CrmQuoteServiceImpl implements ICrmQuoteService {
         newEntity.setExpireDate(LocalDate.now().plusDays(oldEntity.getValidityDays()));
         newEntity.setOwnerUserId(oldEntity.getOwnerUserId());
         newEntity.setOwnerUserName(oldEntity.getOwnerUserName());
+        newEntity.setQuoteType(StringUtils.isNotBlank(oldEntity.getQuoteType()) ? oldEntity.getQuoteType() : "sales");
         newEntity.setPaymentTerms(oldEntity.getPaymentTerms());
         newEntity.setDeliveryTerms(oldEntity.getDeliveryTerms());
         newEntity.setRemark(remark);

+ 1 - 0
backend/ruoyi-modules/ruoyi-crm/src/main/resources/mapper/crm/CrmQuoteMapper.xml

@@ -8,6 +8,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="id" column="id"/>
         <result property="quoteNo" column="quote_no"/>
         <result property="quoteTitle" column="quote_title"/>
+        <result property="quoteType" column="quote_type"/>
         <result property="customerId" column="customer_id"/>
         <result property="customerName" column="customer_name"/>
         <result property="opportunityId" column="opportunity_id"/>

+ 8 - 0
sql/add_quote_type_field.sql

@@ -0,0 +1,8 @@
+ALTER TABLE `crm_quote`
+    ADD COLUMN `quote_type` varchar(20) NOT NULL DEFAULT 'sales' COMMENT '报价单类型(sales-销售/purchase-采购)'
+        AFTER `quote_title`;
+
+UPDATE `crm_quote`
+SET `quote_type` = 'sales'
+WHERE `quote_type` IS NULL OR `quote_type` = '';
+

+ 1 - 0
sql/数据模型-仅结构.sql

@@ -1250,6 +1250,7 @@ CREATE TABLE `crm_quote` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   `quote_no` varchar(50) NOT NULL COMMENT '报价单编号(QUO+yyyyMMdd+4位流水号)',
   `quote_title` varchar(200) NOT NULL COMMENT '报价单标题',
+  `quote_type` varchar(20) NOT NULL DEFAULT 'sales' COMMENT '报价单类型(sales-销售/purchase-采购)',
   `customer_id` bigint(20) NOT NULL COMMENT '客户ID',
   `customer_name` varchar(200) DEFAULT NULL COMMENT '客户名称(冗余字段)',
   `opportunity_id` bigint(20) DEFAULT NULL COMMENT '商机ID',

+ 1 - 0
sql/数据模型-数据和结构.sql

@@ -1584,6 +1584,7 @@ CREATE TABLE `crm_quote` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
   `quote_no` varchar(50) NOT NULL COMMENT '报价单编号(QUO+yyyyMMdd+4位流水号)',
   `quote_title` varchar(200) NOT NULL COMMENT '报价单标题',
+  `quote_type` varchar(20) NOT NULL DEFAULT 'sales' COMMENT '报价单类型(sales-销售/purchase-采购)',
   `customer_id` bigint(20) NOT NULL COMMENT '客户ID',
   `customer_name` varchar(200) DEFAULT NULL COMMENT '客户名称(冗余字段)',
   `opportunity_id` bigint(20) DEFAULT NULL COMMENT '商机ID',

+ 2 - 0
web/src/api/crm/quote.ts

@@ -78,6 +78,7 @@ export interface QuoteForm {
   quoteNo?: string;
   title?: string;
   quoteTitle?: string;
+  quoteType?: string;
   customerId?: string | number;
   customerName?: string;
   opportunityId?: string | number;
@@ -121,6 +122,7 @@ export interface QuoteVO {
   id: string | number;
   quoteNo: string;
   quoteTitle: string;
+  quoteType?: string;
   customerId: string | number;
   customerName: string;
   opportunityId?: string | number;

+ 3 - 0
web/src/api/crm/quote/index.ts

@@ -8,6 +8,7 @@ export interface QuoteQuery {
   pageSize?: number;
   quoteNo?: string;
   quoteTitle?: string;
+  quoteType?: string;
   customerId?: number;
   customerName?: string;
   opportunityId?: number;
@@ -29,6 +30,7 @@ export interface QuoteForm {
   id?: number;
   quoteNo?: string;
   quoteTitle: string;
+  quoteType: string;
   customerId: number;
   customerName?: string;
   opportunityId?: number;
@@ -82,6 +84,7 @@ export interface QuoteVO {
   id: number;
   quoteNo: string;
   quoteTitle: string;
+  quoteType: string;
   customerId: number;
   customerName: string;
   opportunityId?: number;

+ 5 - 0
web/src/views/crm/customer/form.vue

@@ -200,6 +200,11 @@ const rules = ref({
   customerName: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }],
   shortName: [{ required: true, message: '客户简称不能为空', trigger: 'blur' }],
   customerType: [{ required: true, message: '客户类型不能为空', trigger: 'change' }],
+  source: [{ required: true, message: '客户来源不能为空', trigger: 'change' }],
+  parkArea: [{ required: true, message: '所在园区不能为空', trigger: 'change' }],
+  province: [{ required: true, message: '省份不能为空', trigger: 'blur' }],
+  city: [{ required: true, message: '城市不能为空', trigger: 'blur' }],
+  district: [{ required: true, message: '区县不能为空', trigger: 'blur' }],
   mobile: [{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }],
   email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }]
 });

+ 20 - 0
web/src/views/crm/quote/detail.vue

@@ -22,6 +22,11 @@
           <el-descriptions :column="2" border>
             <el-descriptions-item label="报价单号">{{ quote?.quoteNo }}</el-descriptions-item>
             <el-descriptions-item label="报价单标题">{{ quote?.quoteTitle }}</el-descriptions-item>
+            <el-descriptions-item label="报价类型">
+              <el-tag :type="quote?.quoteType === 'purchase' ? 'warning' : 'success'">
+                {{ getQuoteTypeLabel(quote?.quoteType) }}
+              </el-tag>
+            </el-descriptions-item>
             <el-descriptions-item label="客户名称">
               <el-link type="primary" @click="handleViewCustomer">{{ quote?.customerName }}</el-link>
             </el-descriptions-item>
@@ -378,6 +383,18 @@ const quoteContent = ref('');
 // 字典
 const { crm_quote_status, crm_product_unit } = toRefs<any>(proxy?.useDict('crm_quote_status', 'crm_product_unit'));
 
+const quoteTypeMap: Record<string, string> = {
+  sales: '销售',
+  purchase: '采购'
+};
+
+const getQuoteTypeLabel = (type?: string) => {
+  if (!type) {
+    return quoteTypeMap.sales;
+  }
+  return quoteTypeMap[type] || quoteTypeMap.sales;
+};
+
 /** 获取报价单详情 */
 const getDetail = async () => {
   loading.value = true;
@@ -385,6 +402,9 @@ const getDetail = async () => {
     const id = route.query.id as string;
     const res = await getQuote(id); // 直接传字符串,避免精度丢失
     quote.value = res.data;
+    if (quote.value && !quote.value.quoteType) {
+      quote.value.quoteType = 'sales';
+    }
 
     // 加载附件信息
     if (quote.value?.attachments) {

+ 25 - 6
web/src/views/crm/quote/form.vue

@@ -48,6 +48,15 @@
           </el-row>
 
           <el-row :gutter="20">
+        <el-col :span="6">
+          <el-form-item label="报价类型" prop="quoteType" required>
+            <el-radio-group v-model="form.quoteType">
+              <el-radio-button v-for="item in quoteTypeOptions" :key="item.value" :label="item.value">
+                {{ item.label }}
+              </el-radio-button>
+            </el-radio-group>
+          </el-form-item>
+        </el-col>
             <el-col :span="6">
               <el-form-item label="关联商机" prop="opportunityId">
                 <el-input v-model="opportunityDisplayName" placeholder="请选择商机(可选)" readonly clearable>
@@ -71,14 +80,14 @@
                 <el-date-picker v-model="form.quoteDate" type="date" placeholder="选择报价日期" value-format="YYYY-MM-DD" style="width: 100%" />
               </el-form-item>
             </el-col>
-            <el-col :span="6">
-              <el-form-item label="有效期(天)" prop="validityDays" required>
-                <el-input-number v-model="form.validityDays" :min="1" :max="365" style="width: 100%" />
-              </el-form-item>
-            </el-col>
           </el-row>
 
           <el-row :gutter="20">
+        <el-col :span="6">
+          <el-form-item label="有效期(天)" prop="validityDays" required>
+            <el-input-number v-model="form.validityDays" :min="1" :max="365" style="width: 100%" />
+          </el-form-item>
+        </el-col>
             <el-col :span="6">
               <el-form-item label="报价主体" prop="supplierId" required>
                 <el-select v-model="form.supplierId" placeholder="请选择报价主体" clearable style="width: 100%" @change="handleSupplierChange">
@@ -400,6 +409,11 @@ const { proxy } = getCurrentInstance() as any;
 // 字典数据
 const { crm_product_unit, crm_shfs } = toRefs<any>(proxy?.useDict('crm_product_unit', 'crm_shfs'));
 
+const quoteTypeOptions = [
+  { label: '销售', value: 'sales' },
+  { label: '采购', value: 'purchase' }
+];
+
 // 表单标题
 const formTitle = computed(() => {
   return form.value.id ? '编辑报价单' : '新建报价单';
@@ -410,6 +424,7 @@ const form = ref<QuoteForm>({
   id: undefined,
   quoteNo: undefined,
   quoteTitle: '',
+  quoteType: 'sales',
   customerId: undefined,
   customerName: '',
   contactId: undefined,
@@ -435,7 +450,8 @@ const rules = ref({
   quoteDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }],
   validityDays: [{ required: true, message: '请输入有效期', trigger: 'blur' }],
   ownerUserId: [{ required: true, message: '请选择负责人', trigger: 'change' }],
-  supplierId: [{ required: true, message: '请选择报价主体', trigger: 'change' }]
+  supplierId: [{ required: true, message: '请选择报价主体', trigger: 'change' }],
+  quoteType: [{ required: true, message: '请选择报价类型', trigger: 'change' }]
 });
 
 // 显示用的变量
@@ -591,6 +607,9 @@ const getQuoteInfo = async (id: string | number) => {
 
     // 基本信息赋值
     Object.assign(form.value, quoteData);
+    if (!form.value.quoteType) {
+      form.value.quoteType = 'sales';
+    }
 
     // 加载客户简称(从报价主题中提取,或从客户信息中获取)
     // 报价主题格式:供方简称-客户简称(报价单号)

+ 67 - 7
web/src/views/crm/quote/index.vue

@@ -139,6 +139,13 @@
             <el-link type="primary" @click="handleView(scope.row)">{{ scope.row.quoteTitle }}</el-link>
           </template>
         </el-table-column>
+        <el-table-column v-if="isColumnVisible('quoteType')" label="报价类型" align="center" prop="quoteType" width="110">
+          <template #default="scope">
+            <el-tag :type="getQuoteTypeTag(scope.row.quoteType)">
+              {{ getQuoteTypeLabel(scope.row.quoteType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
         <el-table-column
           v-if="isColumnVisible('customerName')"
           label="客户名称"
@@ -490,6 +497,11 @@
             <el-descriptions :column="2" border>
               <el-descriptions-item label="报价单号">{{ detailQuote?.quoteNo }}</el-descriptions-item>
               <el-descriptions-item label="报价单标题">{{ detailQuote?.quoteTitle }}</el-descriptions-item>
+            <el-descriptions-item label="报价类型">
+              <el-tag :type="getQuoteTypeTag(detailQuote?.quoteType)">
+                {{ getQuoteTypeLabel(detailQuote?.quoteType) }}
+              </el-tag>
+            </el-descriptions-item>
               <el-descriptions-item label="客户名称">
                 <el-link type="primary" @click="handleDetailViewCustomer">{{ detailQuote?.customerName }}</el-link>
               </el-descriptions-item>
@@ -827,11 +839,31 @@ const userStore = useUserStore();
 // 字典数据
 const { crm_quote_status, crm_product_unit } = toRefs<any>(proxy!.useDict('crm_quote_status', 'crm_product_unit'));
 
+const quoteTypeMap: Record<string, { label: string; tag: 'success' | 'warning' }> = {
+  sales: { label: '销售', tag: 'success' },
+  purchase: { label: '采购', tag: 'warning' }
+};
+
+const getQuoteTypeLabel = (type?: string) => {
+  if (!type) {
+    return quoteTypeMap.sales.label;
+  }
+  return quoteTypeMap[type]?.label || quoteTypeMap.sales.label;
+};
+
+const getQuoteTypeTag = (type?: string) => {
+  if (!type) {
+    return quoteTypeMap.sales.tag;
+  }
+  return quoteTypeMap[type]?.tag || quoteTypeMap.sales.tag;
+};
+
 // 列配置相关
 const COLUMN_STORAGE_KEY = 'crm_quote_visible_columns';
 
 // 定义所有列配置(顺序代表显示顺序)
 const tableColumns = ref([
+  { prop: 'quoteType', label: '报价类型', fixed: false },
   { prop: 'customerName', label: '客户名称', fixed: false },
   { prop: 'opportunityName', label: '商机名称', fixed: false },
   { prop: 'contactName', label: '联系人', fixed: false },
@@ -861,7 +893,7 @@ const tableColumns = ref([
 ]);
 
 // 默认显示的列(现有字段)
-const defaultVisibleColumns = ['customerName', 'finalAmount', 'status', 'validityDays', 'ownerUserName', 'quoteDate', 'createTime'];
+const defaultVisibleColumns = ['quoteType', 'customerName', 'finalAmount', 'status', 'validityDays', 'ownerUserName', 'quoteDate', 'createTime'];
 
 // 当前显示的列
 const visibleColumns = ref<string[]>([]);
@@ -1575,10 +1607,30 @@ const generateQuoteTemplate = (quote: QuoteVO, supplierInfo?: SupplierInfoVO): s
   const supplierShortName = supplierInfo?.shortName || quote.supplierShortName || '';
   const supplierName = supplierInfo?.supplierName || quote.supplierName || '安庆市山峡化工有限公司';
 
+  const quoteType = (quote.quoteType as string) || 'sales';
+  const isPurchaseQuote = quoteType === 'purchase';
+  const quoteTypeLabel = isPurchaseQuote ? '采购' : '销售';
+
+  const customerName = quote.customerName || '';
+  const customerContactName = quote.contactName || '';
+  const customerContactPhone = quote.contactMobile || '';
+  const supplierContactName = quote.ownerUserName || '';
+  const supplierContactPhone = (quote as any).ownerUserMobile || '';
+
+  const demandName = isPurchaseQuote ? supplierName : customerName;
+  const supplyName = isPurchaseQuote ? customerName : supplierName;
+  const demandContactDisplay = addSpaceBetweenChars(
+    isPurchaseQuote ? supplierContactName : customerContactName
+  );
+  const supplyContactDisplayName = addSpaceBetweenChars(
+    isPurchaseQuote ? customerContactName : supplierContactName
+  );
+  const supplyContactPhone = isPurchaseQuote ? customerContactPhone : supplierContactPhone;
+
   // 根据供应商编码决定是否显示Logo和背景图片
   const showLogo = supplierCode === 'SX';
-  // 根据供应商编码决定标题
-  const quoteTitle = supplierCode === 'SX' ? '山峡销售报价单' : `${supplierShortName || supplierName}销售报价单`;
+  const brandName = showLogo ? '山峡' : (supplierShortName || supplierName || '山峡');
+  const quoteTitle = `${brandName}${quoteTypeLabel}报价单`;
   // 根据供应商编码选择印章图片
   const sealImg = supplierCode === 'SX' ? yzImg : supplierCode === 'YD' ? ydImg : null;
 
@@ -1628,12 +1680,14 @@ const generateQuoteTemplate = (quote: QuoteVO, supplierInfo?: SupplierInfoVO): s
               <div style="flex: 1; text-align: left;">编&nbsp;&nbsp;号:&nbsp;&nbsp;${quote.quoteNo}</div>
             </div>
             <div style="display: flex; justify-content: space-between; margin-bottom: 3px;">
-              <div style="flex: 1; text-align: left;">需&nbsp;&nbsp;方:&nbsp;&nbsp;${quote.customerName}</div>
-              <div style="flex: 1; text-align: left;">供&nbsp;&nbsp;方:&nbsp;&nbsp;${supplierName}</div>
+              <div style="flex: 1; text-align: left;">需&nbsp;&nbsp;方:&nbsp;&nbsp;${demandName}</div>
+              <div style="flex: 1; text-align: left;">供&nbsp;&nbsp;方:&nbsp;&nbsp;${supplyName}</div>
             </div>
             <div style="display: flex; justify-content: space-between;">
-              <div style="flex: 1; text-align: left;">联系人:&nbsp;&nbsp;${addSpaceBetweenChars(quote.contactName || '')}</div>
-              <div style="flex: 1; text-align: left;">联系人:&nbsp;&nbsp;${addSpaceBetweenChars(quote.ownerUserName || '')} / ${(quote as any).ownerUserMobile || ''}</div>
+              <div style="flex: 1; text-align: left;">联系人:&nbsp;&nbsp;${demandContactDisplay}</div>
+              <div style="flex: 1; text-align: left;">
+                联系人:&nbsp;&nbsp;${supplyContactDisplayName}${supplyContactPhone ? ` / ${supplyContactPhone}` : ''}
+              </div>
             </div>
           </div>
         </div>
@@ -1734,6 +1788,9 @@ const handleGenerateQuote = async (row?: QuoteVO) => {
     // 获取完整的报价单详情(包含产品列表)
     const res = await getQuote(id);
     const quoteDetail = res.data;
+    if (!quoteDetail.quoteType) {
+      quoteDetail.quoteType = 'sales';
+    }
 
     console.log('报价单详情数据:', quoteDetail);
 
@@ -2065,6 +2122,9 @@ const getDetailForDrawer = async () => {
   try {
     const res = await getQuote(currentDetailId.value);
     detailQuote.value = res.data;
+    if (detailQuote.value && !detailQuote.value.quoteType) {
+      detailQuote.value.quoteType = 'sales';
+    }
 
     // 加载附件信息
     if (detailQuote.value?.attachments) {