Authored by zhangwenxue

feature: finance/invoice-manage

import UpdateInvoice from './update-invoice';
export { UpdateInvoice };
... ...
<template>
<Modal v-model="visible" :title="title" @on-ok="ok" @on-cancel="cancel">
<Form :model="formItem" :label-width="80">
<FormItem label="订单编号">
<i-input v-model="formItem.orderCode" readonly></i-input>
</FormItem>
<FormItem label="发票类型">
<RadioGroup v-model="formItem.type">
<Radio :label="ELECTRONIC">电子发票</Radio>
<Radio :label="PAPER">纸质发票</Radio>
<Radio v-if="formItem.status !== UNOPEN" :label="CANCELLED">作废</Radio>
</RadioGroup>
</FormItem>
<template v-if="formItem.status !== CANCELLED">
<template v-if="formItem.type == ELECTRONIC">
<FormItem label="上传PDF">
<drag-file-upload @success="uploadPDFSuccess" @remove="uploadPDFRemove"> </drag-file-upload>
</FormItem>
</template>
<template v-else>
<RadioGroup v-model="formItem.logisticsSwitch">
<Radio :label="LOGISTICS_YES">需要物流</Radio>
<Radio :label="LOGISTICS_NO">不需要物流</Radio>
</RadioGroup>
<FormItem v-if="formItem.logisticsSwitch" label=" ">
<Select v-model="formItem.logisticsCompanyName">
<Option v-for="(item, idx) in expressList" :key="idx" :value="item.companyName">
{item.companyName}
</Option>
</Select>
<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" long :loading="inLoading" @click="update">保存</Button>
<Button long @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';
// TODO: move to vuex
let ExpressCompanyList = [];
export default {
name: 'UpdateInvoice',
data() {
return {
visible: false,
title: '',
// constants
UNOPEN: InvoiceStatusName2Id.UNOPEN,
CANCELLED: InvoiceStatusName2Id.CANCELLED,
...InvoiceTypeName2Id,
LOGISTICS_NO: LogisticsTypeName2Id.NO,
LOGISTICS_YES: LogisticsTypeName2Id.YES,
// api result data
expressList: ExpressCompanyList,
formItem: {},
};
},
created() {
this.invoiceAPI = new InvoiceService();
this.tradeAPI = new TradeService();
if (this.expressList.length === 0) {
this.tradeAPI.allotExpressCompList().then(({ data }) => {
this.expressList = ExpressCompanyList = data;
});
}
},
methods: {
setTitle(invoice) {
if (invoice.status === InvoiceStatusName2Id.UNOPEN) {
this.title = '录入发票';
} else {
this.title = '发票详情';
}
},
show(invoice) {
const params = _.pick(invoice, ['shopId', 'orderCode']);
this.setTitle(invoice);
this.invoiceAPI.get(params).then(({ data }) => {
// initial data
this.invoiceData = data;
this.formItem = { ...data };
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 === isCancled) {
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;
},
update() {
const params = { ...this.formItem };
// validate
// TODO:
// update status
if (!this.updateState(params)) {
this.close(true);
return;
}
this.invoiceAPI
.update(params)
.then(() => {
this.$emit('updated', params);
this.close();
})
.catch(() => {
this.$methods.warn('更新失败');
});
},
ok() {
debugger;
},
cancel() {
debugger;
},
close(force) {
if (!force) {
// dirty check
}
this.visible = false;
},
uploadPDFSuccess(attach, res) {
this.formItem.pdfUrl = res.url;
},
uploadPDFRemove() {
this.formItem.pdfUrl = '';
},
},
};
</script>
... ...
export default [
{
path: '/invoice.html',
name: 'invoice',
component: () => import(/* webpackChunkName: "finance.invoice" */ './invoice'),
meta: {
pageName: '发票管理',
},
},
];
... ...
<template>
<layout-body>
<layout-filter>
<filter-item :label="filters.orderCode.label">
<i-input v-model.trim="filters.orderCode.model" :placeholder="filters.orderCode.holder" />
</filter-item>
<filter-item :label="filters.createTime.label">
<Date-picker
v-model="filters.createTime.model"
placement="bottom-end"
type="datetimerange"
format="yyyy-MM-dd HH:mm"
placeholder="选择日期时间"
/>
</filter-item>
<filter-item>
<Button type="primary" @click="search">
查询
</Button>
<Button @click="reset">
清空条件
</Button>
<Button type="warning" class="table-btn" @click="exportList">
导出
</Button>
</filter-item>
</layout-filter>
<layout-tab>
<Tabs :value="activate" :animated="false" @on-click="switchTab">
<Tab-pane :label="noOpenTabLabel" name="UNOPEN"></Tab-pane>
<Tab-pane :label="returnNoOptTabLabel" name="REJECT_UNHANDLED"></Tab-pane>
<Tab-pane :label="openTabLabel" name="OPNED"></Tab-pane>
<Tab-pane label="全部" name="all"></Tab-pane>
</Tabs>
</layout-tab>
<layout-list>
<Table border :columns="tableCols" :data="tableData" />
<Page :total="pageData.total" :current="pageData.pageNo" :page-size="20" show-total @on-change="pageChange" />
</layout-list>
<invoice-update ref="modal" @updated="invoiceUpdated" />
</layout-body>
</template>
<script>
import list from './store/list';
import filter from './store/filter';
import { InvoiceStatusName2Id } from './store/constant';
import InvoiceService from 'services/finance/invoice-service';
import InvoiceUpdate from './components/invoice-update';
import _ from 'lodash';
import moment from 'moment';
// 格式化时间范围为接口需要的YYYY-MM-DD HH:mm:ss
function timeFormat(time) {
if (time === 0) {
return 0;
}
return moment(time).format('YYYY-MM-DD HH:mm:ss');
}
export default {
components: {
InvoiceUpdate,
},
data() {
return {
...list.call(this),
...filter,
activate: 'all', // 当前tabname, all|UNOPEN|REJECT_UNHANDLED|OPNED
noOpenInvoiceSum: 0, // 未开票
openInvoiceSum: 0, // 已开票
returnNoOptionSum: 0, // 退货待处理
};
},
computed: {
startTime() {
const createTime = this.filters.createTime.model;
if (_.isEmpty(createTime)) {
return 0;
} else {
return timeFormat(createTime[0]);
}
},
endTime() {
const createTime = this.filters.createTime.model;
if (_.isEmpty(createTime)) {
return 0;
} else {
return timeFormat(createTime[1]);
}
},
noOpenTabLabel() {
return `待开票${this.noOpenInvoiceSum === 0 ? '' : '(' + this.noOpenInvoiceSum + ')'}`;
},
returnNoOptTabLabel() {
return `退货待处理${this.returnNoOptionSum === 0 ? '' : '(' + this.returnNoOptionSum + ')'}`;
},
openTabLabel() {
return `已开票${this.openInvoiceSum === 0 ? '' : '(' + this.openInvoiceSum + ')'}`;
},
},
watch: {
$route() {
this.setActiveTab();
},
},
created() {
this.invoiceService = new InvoiceService();
this.setActiveTab();
// this.search();
},
methods: {
switchTab(name) {
this.$router.replace({
name: `finance.invoice`,
query: {
tab: name,
},
});
},
setActiveTab() {
const { tab } = this.$route.query;
this.activate = tab || 'all';
this.search();
},
filterValues() {
const params = {
orderCode: +this.filters.orderCode.model,
shopId: this.$user.currentShop.shopsId,
beginTime: this.startTime,
endTime: this.endTime,
pageSize: this.pageData.pageSize,
pageNo: this.pageData.pageNo,
};
if (this.activate !== 'all') {
params.status = InvoiceStatusName2Id[this.activate];
}
return _.pickBy(params, val => val);
},
search() {
this.invoiceService.query(this.filterValues()).then(({ data }) => {
this.tableData = _.get(data, 'pageData.records', []);
this.pageData.total = _.get(data, 'pageData.totalCount', 0);
this.pageData.pageNo = _.get(data, 'pageData.pageNo', 1);
this.noOpenInvoiceSum = _.get(data, 'noOpenInvoiceSum', 0);
this.returnNoOptionSum = _.get(data, 'returnNoOptionSum', 0);
this.openInvoiceSum = _.get(data, 'openInvoiceSum', 0);
});
},
reset() {
this.filters.orderCode.model = null;
this.filters.createTime.model = null;
this.pageData.pageNo = 1;
this.pageData.total = 0;
this.search();
},
pageChange(val) {
this.pageData.pageNo = val;
this.search();
},
onClickInfo(params) {
console.log('onClickInfo', params);
},
exportList() {
// TODO: check timerange
// 30天
const params = { ...this.filterValues() };
let msg;
if (params.endTime === 0 || params.startTime === 0) {
// timeRange is required
msg = `请选择需要导出的时间范围`;
} else {
const range = new Date(params.endTime) - new Date(params.startTime);
if (range > 30 * 24 * 3600 * 1000) {
// 30days
// timeRange is limited in 30 days
msg = `最多允许导出连续30天的发票列表, 请把时间范围调整不多于30天`;
}
}
if (msg) {
this.$Message.warning(msg);
return;
}
const href = this.invoiceService.getExportUrl(params);
window.open(href, '_blank');
},
invoiceUpdated(updateInvoiceInfo) {
console.log(updateInvoiceInfo);
debugger;
},
},
};
</script>
<style lang="scss" scoped>
.text-red {
color: #ed3f14;
}
</style>
... ...
// TODO: add test
// const invoiceStatus = {
// 20: ['未开票', 'UNOPEN'],
// 30: ['已开票', 'OPNED'],
// 40: ['退货待处理', 'REJECT_UNHANDLED'],
// 50: ['退货已处理', 'REJECT_HANDLED'],
// 90: ['已作废', 'CANCELLED'],
// };
// _.forEach(invoiceStatus, (value, key) => {
// InvoiceStatusId2CNName[key] = value[0];
// InvoiceStatusName2Id[value[1]] = key;
// });
//发票状态 20、未开票 30、已开票 40、退货待处理 50、退货已处理 90、已作废
/**
* id => CNName 用于数据显示
*/
const InvoiceStatusId2CNName = {
20: '未开票',
30: '已开票',
40: '退货待处理',
50: '退货已处理',
90: '已作废',
};
/**
* name => id 做为枚举常量
*/
const InvoiceStatusName2Id = {
UNOPEN: 20,
OPNED: 30,
REJECT_UNHANDLED: 40,
REJECT_HANDLED: 50,
CANCELLED: 90,
};
//发票类型 1:纸质票 2:电子票
/**
* id => CNName 用于数据显示
*/
const InvoiceTypeId2CNName = {
1: '纸质票',
2: '电子票',
};
/**
* name => id 做为枚举常量
*/
const InvoiceTypeName2Id = {
PAPER: 1,
ELECTRONIC: 2,
};
/**
* logisticsSwitch '1 需要物流 2 不需要物流'
*/
const LogisticsTypeName2Id = {
NO: 2,
YES: 1,
};
export { InvoiceStatusId2CNName, InvoiceStatusName2Id, InvoiceTypeId2CNName, InvoiceTypeName2Id, LogisticsTypeName2Id };
... ...
const date = new Date();
const agoMonthFirstDay = new Date(date.getFullYear(), date.getMonth() - 1, date.getDate()); // TODO: check edge 0~24?
const agoMonthLastDay = new Date(date.getFullYear(), date.getMonth(), date.getDate());
export default {
filters: {
orderCode: {
label: '订单号',
labelSpan: 6,
model: '',
holder: '订单号',
fieldSpan: 18,
},
createTime: {
label: '创建时间',
labelSpan: 6,
model: [agoMonthFirstDay, agoMonthLastDay],
holder: '',
fieldSpan: 18,
},
},
};
... ...
/**
* on clearing page store
* @author: wsl <shuiling.wang@yoho.cn>
* @date: 2017/07/11
*/
import moment from 'moment';
import { InvoiceTypeId2CNName, InvoiceStatusId2CNName, InvoiceStatusName2Id } from './constant';
export default function() {
return {
tableCols: [
{
title: '订单号',
key: 'orderCode',
align: 'center',
},
{
title: '订单金额',
key: 'invoiceAmount',
align: 'center',
},
{
title: '退货金额',
key: 'returnAmount',
align: 'center',
render(h, parmas) {
const { returnAccount } = parmas.row;
if (returnAccount === 0) {
return '-';
} else {
return <span class="text-red">({{ returnAccount }})</span>;
}
},
},
{
title: '开票时间',
key: 'createTimeString',
align: 'center',
render(h, params) {
const { issueDate } = params.row;
if (issueDate) {
const time = moment(issueDate);
return (
<div>
<div> {time.format('YYYY/MM/DD')} </div>
<div> {time.format('HH:mm:ss')} </div>
</div>
);
} else {
return '';
}
},
},
{
title: '开票类型',
key: 'type',
align: 'center',
render: (h, params) => {
return InvoiceTypeId2CNName[params.row.type] || '-';
},
},
{
title: '发票状态',
key: 'status',
align: 'center',
render(h, params) {
return InvoiceStatusId2CNName[params.row.status] || '-';
},
},
{
title: '发票抬头',
key: 'invoicePayable',
align: 'center',
},
{
title: '纳税人设别号',
key: 'buyerTaxNumber',
align: 'center',
},
{
title: '操作',
key: 'action',
width: 250,
align: 'center',
render: (h, params) => {
const { status } = params.row;
let btnType;
let btnText;
if (status !== InvoiceStatusName2Id.UNOPEN) {
btnType = 'info';
btnText = '查看详情';
} else {
btnType = 'primary';
btnType = '录入发票';
}
return (
<div class="action-btn-row">
<i-button type={btnType} size="small" onClick={() => this.onClickInfo(params)}>
{btnText}
</i-button>
</div>
);
},
},
],
tableData: [],
pageData: {
total: 0,
pageNo: 0,
pageSize: 20,
},
};
}
... ...
import FinanceService from './finance-service';
import InvoiceService from './invoice-service';
export {
FinanceService
};
export { FinanceService, InvoiceService };
... ...
import Service from '../service';
import qs from 'querystringify';
const apiUrl = {
queryPageInvoice: '/erp/queryPageInvoice',
queryInvoice: '/erp/queryInvoice',
updateInvoice: '/erp/updateInvoice',
exportInvoiceList: '/erp/exportInvoiceList',
};
class InvoiceService extends Service {
queryList(params) {
return this.post(apiUrl.queryPageInvoice, params);
}
get(params) {
return this.post(apiUrl.queryInvoice, params);
}
update(params) {
return this.post(apiUrl.updateInvoice, params);
}
getExportUrl(params) {
const querystring = qs.stringify(params);
return `${apiUrl.exportInvoiceList}?${querystring}`;
}
}
export default InvoiceService;
... ...
... ... @@ -66,6 +66,13 @@ const domainApis = {
favoriteBalanceCheck: '/erp-gateway-web/finance/favoriteBalanceDetail/check',
clearingList: '/erp-gateway-web/favorite/clearing/list',
clearingDetail: '/erp-gateway-web/favorite/clearingDetail/list',
// 发票管理
queryPageInvoice: '/erp-gateway-web/franchiseInvoice/queryPageInvoice', // 订单发票列表接口
queryInvoice: '/erp-gateway-web/franchiseInvoice/queryInvoice', // 订单发票详情接口
updateInvoice: '/erp-gateway-web/s/franchiseInvoice/updateInvoice', // 发票信息更改接口
exportInvoiceList: '/erp-gateway-web/s/franchiseInvoice/exportInvoiceList', // 导出发票列表
queryByPidForFavorite: '/erp-gateway-web/account/profile/queryByPidForFavorite',
exportFavoriteClearingList: '/erp-gateway-web/export/favoriteClearingList',
exportFavoriteClearingDetail: '/erp-gateway-web/export/favoriteClearingDetail',
... ...