Pārlūkot izejas kodu

开票功能紧急更新

skyline 3 dienas atpakaļ
vecāks
revīzija
41803f2d66

+ 152 - 0
admin-web/src/views/admin/invoice/confirmManualDialog.vue

@@ -0,0 +1,152 @@
+<style scoped lang="scss">
+.dialog-body {
+  .info-section {
+    margin-bottom: 16px;
+  }
+  .info-title {
+    font-size: 14px;
+    font-weight: bold;
+    color: #303133;
+    margin-bottom: 12px;
+    padding-left: 10px;
+    border-left: 3px solid #409eff;
+  }
+  .warning-tip {
+    margin-top: 16px;
+    padding: 10px 16px;
+    background: #fdf6ec;
+    border: 1px solid #e6a23c;
+    border-radius: 4px;
+    color: #e6a23c;
+    font-size: 13px;
+    line-height: 1.6;
+  }
+}
+</style>
+<template>
+  <div class="system-dialog-container">
+    <el-dialog
+        title="确认手动开票"
+        v-model="state.dialog.isShowDialog"
+        width="700px"
+        draggable
+        destroy-on-close
+        :close-on-click-modal="false"
+        @close="onClose"
+        align-center>
+      <div class="dialog-body" v-if="state.invoice">
+        <div class="info-section">
+          <div class="info-title">发票基本信息</div>
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item label="发票ID">{{ state.invoice.id }}</el-descriptions-item>
+            <el-descriptions-item label="发票申请单号">{{ state.invoice.applyId }}</el-descriptions-item>
+            <el-descriptions-item label="发票类型">
+              <ext-d-label type="Invoice.type" :model-value="state.invoice.invoiceType"/>
+            </el-descriptions-item>
+            <el-descriptions-item label="开票金额">{{ u.fmt.fmtMoney(state.invoice.invoiceAmount) }}</el-descriptions-item>
+            <el-descriptions-item label="订单总额">{{ u.fmt.fmtMoney(state.invoice.totalMoney) }}</el-descriptions-item>
+            <el-descriptions-item label="总电量(度)">{{ state.invoice.totalPower }}</el-descriptions-item>
+            <el-descriptions-item label="电费">{{ u.fmt.fmtMoney(state.invoice.elecMoney) }}</el-descriptions-item>
+            <el-descriptions-item label="服务费">{{ u.fmt.fmtMoney(state.invoice.serviceMoney) }}</el-descriptions-item>
+            <el-descriptions-item label="服务费优惠">{{ u.fmt.fmtMoney(state.invoice.serviceMoneyDiscount) }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <div class="info-section">
+          <div class="info-title">购买方信息</div>
+          <el-descriptions :column="2" border size="small">
+            <el-descriptions-item label="发票抬头">{{ state.invoice.invoiceTitle || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="公司税号">{{ state.invoice.taxId || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="公司地址">{{ state.invoice.address || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="联系电话">{{ state.invoice.telephone || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="开户银行">{{ state.invoice.bankName || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="银行账户">{{ state.invoice.bankAccount || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="接收邮箱">{{ state.invoice.email || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="电话">{{ state.invoice.phone || '-' }}</el-descriptions-item>
+          </el-descriptions>
+        </div>
+
+        <div class="info-section" v-if="state.invoice.orderDetails && state.invoice.orderDetails.length > 0">
+          <div class="info-title">关联订单</div>
+          <el-table :data="state.invoice.orderDetails" border size="small">
+            <el-table-column label="充电订单号" prop="startChargeSeq"/>
+            <el-table-column label="总电量(度)" prop="totalPower"/>
+            <el-table-column label="订单总额">
+              <template #default="{row}">{{ u.fmt.fmtMoney(row.totalMoney) }}</template>
+            </el-table-column>
+            <el-table-column label="电费">
+              <template #default="{row}">{{ u.fmt.fmtMoney(row.elecMoney) }}</template>
+            </el-table-column>
+            <el-table-column label="服务费">
+              <template #default="{row}">{{ u.fmt.fmtMoney(row.serviceMoney) }}</template>
+            </el-table-column>
+            <el-table-column label="服务费优惠">
+              <template #default="{row}">{{ u.fmt.fmtMoney(row.serviceMoneyDiscount) }}</template>
+            </el-table-column>
+          </el-table>
+        </div>
+
+        <div class="warning-tip">
+          请仔细核对以上发票信息与手动开票数据是否一致。确认后系统将自动完成开票后处理(更新发票状态、关联订单开票状态等),此操作不可撤销。
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="onCancel" size="default">取 消</el-button>
+          <el-button :loading="state.btnLoading" type="primary" @click="onConfirm" size="default">确认已手动开票</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup lang="ts" name="ConfirmManualDialog">
+import { reactive, ref } from 'vue';
+import { Msg } from "/@/utils/message";
+import { $post } from "/@/utils/request";
+import u from '/@/utils/u'
+import ExtDLabel from "/@/components/form/ExtDLabel.vue";
+
+const emit = defineEmits(['refresh']);
+
+const initState = () => ({
+  invoice: null as any,
+  btnLoading: false,
+  dialog: {
+    isShowDialog: false,
+  },
+});
+
+const state = reactive(initState());
+
+const open = (row: any) => {
+  state.invoice = row;
+  state.dialog.isShowDialog = true;
+};
+
+const onClose = () => {
+  state.dialog.isShowDialog = false;
+  Object.assign(state, initState());
+};
+
+const onCancel = () => {
+  onClose();
+};
+
+const onConfirm = () => {
+  state.btnLoading = true;
+  $post(`/finance/confirmManualInvoice/${state.invoice.id}`).then(() => {
+    state.btnLoading = false;
+    Msg.message('手动开票确认成功');
+    onClose();
+    emit('refresh');
+  }).catch(() => {
+    state.btnLoading = false;
+  });
+};
+
+defineExpose({
+  open
+});
+</script>

+ 10 - 2
admin-web/src/views/admin/invoice/index.vue

@@ -187,7 +187,8 @@
             </template>
             <template v-else-if="field.prop==='action'">
               <el-button v-if="row.status===0" v-auth="'invoice.modify'" size="small" plain type="warning" @click="handleInvice(row)">开票</el-button>
-              <el-button v-if="row.status===0" v-auth="'invoice.modify'" size="small" plain type="danger" @click="handleCancelInvoice(row)">取消开票</el-button>
+              <el-button v-if="row.status===0||row.status===3" v-auth="'invoice.modify'" size="small" plain type="danger" @click="handleCancelInvoice(row)">取消开票</el-button>
+              <el-button v-if="row.status===0||row.status===3" v-auth="'invoice.modify'" size="small" plain type="primary" @click="handleConfirmManual(row)">已手动开票</el-button>
               <el-button v-if="row.status===1" v-auth="'invoice.modify'" size="small" plain type="success" @click="previewInvoice(row)">下载</el-button>
               <el-button   size="small" plain type="success" @click="handleInfo(row)">详情</el-button>
             </template>
@@ -203,6 +204,7 @@
     </el-card>
   </div>
   <InvoiceDialog ref="invoiceDialogRef" @refresh="loadData(true)"/>
+  <ConfirmManualDialog ref="confirmManualDialogRef" @refresh="loadData(true)"/>
 
   <canvas ref="pdfViewer"></canvas>
 </template>
@@ -222,12 +224,14 @@ import ExtDLabel from "/@/components/form/ExtDLabel.vue";
 import ExtDatePicker from "/@/components/form/ExtDatePicker.vue";
 
 const InvoiceDialog = defineAsyncComponent(() => import("/@/views/admin/invoice/dialog.vue"));
+const ConfirmManualDialog = defineAsyncComponent(() => import("/@/views/admin/invoice/confirmManualDialog.vue"));
 
 // import pdfjsLib from 'pdfjs-dist'
 
 //定义引用
 const queryRef = ref();
 const invoiceDialogRef = ref();
+const confirmManualDialogRef = ref();
 
 const pdfViewer = ref()
 
@@ -278,7 +282,7 @@ const state = reactive({
       {label: '创建时间', prop: 'createTime', sortable: 'custom', resizable: true, width: 170},
       {label: '更新时间', prop: 'updateTime', sortable: 'custom', resizable: true, width: 170},
       {
-        label: '操作', prop: 'action', width: 240, align: 'center', fixed: 'right',
+        label: '操作', prop: 'action', width: 320, align: 'center', fixed: 'right',
       }
     ],
   },
@@ -420,6 +424,10 @@ const handleCancelInvoice = (row:any) => {
 
 }
 
+const handleConfirmManual = (row: any) => {
+  confirmManualDialogRef.value.open(row);
+}
+
 
 //endregion
 

+ 7 - 0
admin/src/main/java/com/kym/admin/controller/FinanceController.java

@@ -107,6 +107,13 @@ public class FinanceController {
         return R.success();
     }
 
+    @SysLog("确认手动开票")
+    @PostMapping("/confirmManualInvoice/{invoiceId}")
+    R<?> confirmManualInvoice(@PathVariable("invoiceId") Long invoiceId) {
+        invoiceService.confirmManualInvoice(invoiceId);
+        return R.success();
+    }
+
     @SysLog("下载发票")
     @GetMapping("/downloadInvoice/{invoiceId}")
     R<?> downloadInvoice(@PathVariable("invoiceId") String invoiceId) {

+ 2 - 0
service/src/main/java/com/kym/service/admin/InvoiceDetailService.java

@@ -23,4 +23,6 @@ public interface InvoiceDetailService extends IService<InvoiceDetail> {
     ExcelWriter exportInvoiceDetail(InvoiceDetailQueryParam params);
 
     void updateInvoiceDetail(String applyId, FapiaoApplications fapiaoApplications);
+
+    void createManualInvoiceDetail(InvoiceDetail invoiceDetail);
 }

+ 7 - 0
service/src/main/java/com/kym/service/admin/impl/InvoiceDetailServiceImpl.java

@@ -143,4 +143,11 @@ public class InvoiceDetailServiceImpl extends MPJBaseServiceImpl<InvoiceDetailMa
             save(invoiceDetail);
         }
     }
+
+    @Override
+    @DS("db-admin")
+    public void createManualInvoiceDetail(InvoiceDetail invoiceDetail) {
+        removeByMap(Map.of("apply_id", invoiceDetail.getApplyId()));
+        save(invoiceDetail);
+    }
 }

+ 2 - 0
service/src/main/java/com/kym/service/miniapp/InvoiceService.java

@@ -29,4 +29,6 @@ public interface InvoiceService extends MPJBaseService<Invoice> {
 
     void cancelApplyInvoice(String invoiceId);
 
+    void confirmManualInvoice(Long invoiceId);
+
 }

+ 57 - 2
service/src/main/java/com/kym/service/miniapp/impl/InvoiceServiceImpl.java

@@ -9,6 +9,9 @@ import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import com.kym.common.exception.BusinessException;
 import com.kym.common.utils.CommUtil;
 import com.kym.common.utils.OrderUtils;
+import com.baomidou.dynamic.datasource.annotation.DSTransactional;
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+import com.kym.common.exception.BusinessException;
 import com.kym.entity.admin.InvoiceDetail;
 import com.kym.entity.admin.queryParams.InvoiceQueryParam;
 import com.kym.entity.common.PageBean;
@@ -17,9 +20,11 @@ import com.kym.entity.miniapp.Invoice;
 import com.kym.entity.miniapp.User;
 import com.kym.entity.miniapp.queryParams.ApplyInvoiceParams;
 import com.kym.entity.miniapp.vo.InvoiceVo;
+import com.kym.entity.wechat.FaPiao;
 import com.kym.entity.wechat.InvoiceOrderDetail;
 import com.kym.entity.wechat.TitleUrl;
 import com.kym.mapper.miniapp.InvoiceMapper;
+import com.kym.service.admin.InvoiceDetailService;
 import com.kym.service.miniapp.ChargeOrderService;
 import com.kym.service.miniapp.DataDictService;
 import com.kym.service.miniapp.InvoiceService;
@@ -29,6 +34,7 @@ import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDateTime;
 import java.util.Arrays;
 import java.util.List;
 
@@ -48,11 +54,13 @@ public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoic
     private final ChargeOrderService chargeOrderService;
     private final WxPayService wxPayService;
     private final DataDictService dataDictService;
+    private final InvoiceDetailService invoiceDetailService;
 
-    public InvoiceServiceImpl(ChargeOrderService chargeOrderService, @Lazy WxPayService wxPayService, @Lazy DataDictService dataDictService) {
+    public InvoiceServiceImpl(ChargeOrderService chargeOrderService, @Lazy WxPayService wxPayService, @Lazy DataDictService dataDictService, InvoiceDetailService invoiceDetailService) {
         this.dataDictService = dataDictService;
         this.chargeOrderService = chargeOrderService;
         this.wxPayService = wxPayService;
+        this.invoiceDetailService = invoiceDetailService;
     }
 
 
@@ -162,7 +170,9 @@ public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoic
     @Override
     @Transactional
     public void cancelApplyInvoice(String invoiceId) {
-        var invoice = lambdaQuery().eq(Invoice::getId, invoiceId).eq(Invoice::getStatus, Invoice.STATUS_待开票).orderByDesc(Invoice::getId).one();
+        var invoice = lambdaQuery().eq(Invoice::getId, invoiceId)
+                .in(Invoice::getStatus, List.of(Invoice.STATUS_待开票, Invoice.STATUS_开票中))
+                .orderByDesc(Invoice::getId).one();
         if (!CommUtil.isEmptyOrNull(invoice)) {
             // 订单发票状态修改为待开票
             var startChargeSeqs = invoice.getOrderDetails().stream().map(InvoiceOrderDetail::getStartChargeSeq).toList();
@@ -172,4 +182,49 @@ public class InvoiceServiceImpl extends MPJBaseServiceImpl<InvoiceMapper, Invoic
         }
     }
 
+    @Override
+    @DSTransactional
+    public void confirmManualInvoice(Long invoiceId) {
+        var invoice = lambdaQuery().eq(Invoice::getId, invoiceId).one();
+        if (invoice == null) {
+            throw new BusinessException("发票记录不存在");
+        }
+        if (!Arrays.asList(Invoice.STATUS_开票中, Invoice.STATUS_待开票).contains(invoice.getStatus())) {
+            throw new BusinessException("仅待开票或开票中状态的发票可确认手动开票");
+        }
+
+        var startChargeSeqs = invoice.getOrderDetails().stream().map(InvoiceOrderDetail::getStartChargeSeq).toList();
+
+        // 更新订单开票状态为已开票
+        chargeOrderService.lambdaUpdate()
+                .set(ChargeOrder::getInvoiceStatus, ChargeOrder.INVOICE_STATUS_已开票)
+                .in(ChargeOrder::getStartChargeSeq, startChargeSeqs)
+                .update();
+
+        // 更新发票状态为已开票
+        lambdaUpdate().set(Invoice::getStatus, Invoice.STATUS_已开票).eq(Invoice::getId, invoice.getId()).update();
+
+        // 写入发票详情记录
+        var invoiceDetail = new InvoiceDetail()
+                .setApplyId(invoice.getApplyId())
+                .setFapiaoTime(LocalDateTime.now())
+                .setStatus("ISSUED")
+                .setTotalAmount(invoice.getInvoiceAmount())
+                .setBuyerInformation(new FaPiao.BuyerInformation()
+                        .setType(invoice.getInvoiceType())
+                        .setName(invoice.getInvoiceTitle())
+                        .setTaxpayer_id(invoice.getTaxId())
+                        .setAddress(invoice.getAddress())
+                        .setTelephone(invoice.getTelephone())
+                        .setBank_name(invoice.getBankName())
+                        .setBank_account(invoice.getBankAccount())
+                        .setPhone(invoice.getPhone())
+                        .setEmail(invoice.getEmail()));
+        DynamicDataSourceContextHolder.push("db-admin");
+        invoiceDetailService.createManualInvoiceDetail(invoiceDetail);
+        DynamicDataSourceContextHolder.poll();
+
+        log.info("手动开票确认完成, invoiceId:{}, applyId:{}, 关联订单:{}", invoiceId, invoice.getApplyId(), startChargeSeqs);
+    }
+
 }