invoice-update.vue 9.08 KB
<template>
  <Modal v-model="visible" :title="title" :loading="inLoading">
    <Form ref="modal" :model="formItem" :label-width="80" :rules="rules">
      <FormItem label="订单编号">
        <span>{{ formItem.orderCode }}</span>
      </FormItem>
      <FormItem label="发票类型">
        <RadioGroup v-model="formItem.type">
          <Radio :label="ELECTRONIC">电子发票</Radio>
          <Radio :label="PAPER">纸质发票</Radio>
          <Radio :label="CANCELLED">作废</Radio>
        </RadioGroup>
      </FormItem>
      <template v-if="formItem.type !== CANCELLED">
        <FormItem v-show="formItem.type === ELECTRONIC" label="上传PDF" prop="pdfUrl">
          <PDFUpload v-model="formItem.pdfUrl" @input="onPDFUrlChang" />
        </FormItem>
        <FormItem v-show="formItem.type === PAPER" label="物流信息">
          <RadioGroup v-model="formItem.logisticsSwitch">
            <Radio :label="LOGISTICS_YES">需要物流</Radio>
            <Radio :label="LOGISTICS_NO">不需要物流</Radio>
          </RadioGroup>
        </FormItem>
        <template v-if="formItem.logisticsSwitch === LOGISTICS_YES && formItem.type === PAPER">
          <FormItem prop="logisticsId" class="logistics-company">
            <Select v-model="formItem.logisticsId">
              <Option v-for="(item, idx) in expressList" :key="idx" :value="item.id">
                {{ item.companyName }}
              </Option>
            </Select>
          </FormItem>
          <FormItem prop="expressNumber" class="express-number">
            <i-input v-model="formItem.expressNumber" type="text" placeholder="物流单号"></i-input>
          </FormItem>
        </template>
      </template>
    </Form>
    <div slot="footer" class="modal-footer">
      <span v-if="formItem.status !== UNOPEN"><i>*</i> 在第三方开票系统冲红或作废后更新保存发票信息</span>
      <Button type="primary" :loading="inLoading" @click="update">保存</Button>
      <Button @click="close">关闭</Button>
    </div>
  </Modal>
</template>

<script>
import _ from 'lodash';
import { InvoiceStatusName2Id, InvoiceTypeName2Id, LogisticsTypeName2Id } from '../store/constant';
import InvoiceService from 'services/finance/invoice-service';
import TradeService from 'services/trade/trade-service';
import PDFUpload from './pdf-upload';

export default {
  name: 'UpdateInvoice',
  components: {
    PDFUpload,
  },
  data() {
    return {
      visible: false,
      inLoading: false,
      title: '',
      // constants
      UNOPEN: InvoiceStatusName2Id.UNOPEN,
      CANCELLED: InvoiceStatusName2Id.CANCELLED,
      ...InvoiceTypeName2Id,
      LOGISTICS_NO: LogisticsTypeName2Id.NO,
      LOGISTICS_YES: LogisticsTypeName2Id.YES,
      // api result data
      expressList: [],
      formItem: {},

      // rules
      rules: {
        pdfUrl: {
          trigger: 'change',
          validator: (rule, value, callback) => {
            if (this.formItem.type === InvoiceTypeName2Id.ELECTRONIC) {
              if (!value) {
                return callback(new Error('请上传发票PDF文件'));
              }
            }
            callback();
          },
        },
        logisticsId: {
          trigger: 'change',
          validator: (rule, value, callback) => {
            if (this.formItem.type === InvoiceTypeName2Id.PAPER) {
              if (!value) {
                return callback(new Error('请选择物流公司'));
              }
            }

            callback();
          },
        },
        expressNumber: {
          trigger: 'blur',
          validator: (rule, value, callback) => {
            if (this.formItem.type === InvoiceTypeName2Id.PAPER) {
              if (!value) {
                return callback(new Error('请输入物流单号'));
              }

              if (!/^[\w\d_-]{5,}$/.test(value)) {
                // TODO: refs https://www.cnblogs.com/Jerseyblog/p/10077136.html
                return callback(new Error('请输入有效的物流单号')); // TODO: 描述下什么是有效的?
              }
            }

            callback();
          },
        },
      },
    };
  },
  created() {
    this.invoiceAPI = new InvoiceService();
    this.tradeAPI = new TradeService();
    if (this.expressList.length === 0) {
      this.tradeAPI.allotExpressCompList().then(data => {
        this.expressList = data;
      });
    }
  },
  methods: {
    setTitle(invoice) {
      if (invoice.status === InvoiceStatusName2Id.UNOPEN) {
        this.title = '录入发票';
      } else {
        this.title = '发票详情';
      }
    },
    show(invoice) {
      // this.$refs.modal.resetFields();
      this.setTitle(invoice);
      // initial data
      this.invoiceData = invoice;
      this.formItem = {
        pdfUrl: null,
        logisticsId: null,
        expressNumber: null,
        logisticsSwitch: this.LOGISTICS_YES,
        ...invoice,
      };
      // transfer CANCELLED state to invoice type
      if (this.formItem.status === InvoiceStatusName2Id.CANCELLED) {
        this.formItem.type = InvoiceStatusName2Id.CANCELLED;
      }
      this.visible = true;
    },
    /**
     * 前端可控制的状态转移
     * Q:
     * 这个作废是什么意思?
     * 选择作废后,下面的 pdf 或物流信息怎么处理
     * 如果发票状态是未开发票,可以先作废吗
     * 选作废后,是不是就不可以编辑当前发票的状态了
     *
     * A:
     * 1.作废就是不提供发票了,之前提供的发票也不展示了
     * 2.删除
     * 3.可以,表示不提供发票
     * 4.可以编辑,作废只是状态之一,不是终点
     *
     * so: rules
     *
     * [1]未开发票->已经开发票
     * [2]退货未处理->退货已经处理
     * [3]作废->已开发票(针对退款额为0)
     * [4]作废->退货已经处理((针对退款额不为0)
     *
     * [5]非作废状态都可作废(*->作废)
     * 退货未处理->作废
     * 退货已经处理->作废
     * 未开发票->作废
     * 已开发票->作废
     * [6] 已开票->已开票, 退货已经处理->退货已经处理(更新发票信息)
     * return: true: 可以提交, false: 不必提交(当前状态为作废, 选择状态还是作废)
     */
    updateState(invoice) {
      const { status: currentStatus } = this.invoiceData;
      const isCancled = invoice.status === InvoiceStatusName2Id.CANCELLED;

      if (isCancled) {
        if (currentStatus === InvoiceStatusName2Id.CANCELLED) {
          return false;
        }

        // 作废, 删除关联数据 [5]
        _.assign(invoice, {
          type: InvoiceTypeName2Id.ELECTRONIC,
          pdfUrl: null,
          logisticsSwitch: null,
          logisticsId: null,
          expressNumber: null,
        });
      } else {
        let nextStatus;
        switch (currentStatus) {
          case InvoiceStatusName2Id.UNOPEN:
            nextStatus = InvoiceStatusName2Id.OPNED; // [1]
            break;
          case InvoiceStatusName2Id.REJECT_UNHANDLED:
            nextStatus = InvoiceStatusName2Id.REJECT_HANDLED; // [2]
            break;
          case InvoiceStatusName2Id.CANCELLED:
            if (this.invoiceData.returnAmount === 0) {
              nextStatus = InvoiceStatusName2Id.OPNED; // [3]
            } else {
              nextStatus = InvoiceStatusName2Id.REJECT_HANDLED; // [4]
            }
            break;
          default:
            nextStatus = currentStatus; // [6]
        }
        invoice.status = nextStatus;
      }

      return true;
    },
    validate(callback) {
      if (this.$refs.modal.fields.length !== 0) {
        return this.$refs.modal.validate(callback);
      }

      return callback(true);
    },
    update() {
      // validate
      this.validate(valid => {
        if (valid) {
          // only whitelist
          const params = _.pick(this.formItem, [
            'shopId',
            'orderCode',
            'status',
            'step',
            // issueDate
            'pdfUrl',
            'type',
            'logisticsId',
            // logisticsCompanyName
            'expressNumber',
            'logisticsSwitch',
          ]);

          // clean data?
          if (!this.updateState(params)) {
            this.close();
            return;
          }

          this.inLoading = true;
          this.invoiceAPI
            .update(params)
            .then(() => {
              this.inLoading = false;
              this.$Message.success('更新成功');
              this.$emit('updated', params);
              this.close();
            })
            .catch(() => {
              this.inLoading = false;
              this.$Message.warning('更新失败');
            });
        }
      });
    },
    close() {
      this.visible = false;
    },
    onPDFUrlChang() {
      this.$refs.modal.validateField('pdfUrl');
    },
  },
};
</script>

<style lang="scss" scoped>
.modal-footer {
  & > span {
    float: left;
    line-height: 32px;
  }
  i {
    color: red;
  }
}
.file-upload {
  display: inline-block;
}

.logistics-company {
  display: inline-block;
  min-width: 20em;
}

.express-number {
  display: inline-block;
  /deep/ .ivu-form-item-content {
    margin-left: 20px !important;
  }
}
</style>