文章目录
1 摘要
合同的收款计划、付款计划是财务系统的常见功能之一,即将合同的总金额按照付款周期多次付清。此功能主要涉及到日期的推算和金额的计算,本文将介绍一种收付款计划生成的代码实现,以供参考。
2 需求
收付款计划需求:
付款周期分为一次性支付、按月支付、按季度支付、按半年支付、按年支付
计划付款日期:
(1)第一次付款按指定的首次付款日期付款
(2)付款周期:按期-月,第二次付款的付款日=首次付款日期中的月份+1,第三次付款的付款日=首次付款日期中的月份+2,以此类推;
若后续月份中没有首次付款日期中指定的“日”,则默认当月最后一日为计划付款日期,如指定首次付款日期为2020年1月31日,
2020年2月份没有31日就以2月份最后一天作为付款日;若指定首次付款日期中的“日”为当月最后一天,则后续月份的计划付款日期也为当月最后一天,
如指定首次付款日期为2020年2月29日,则下次计划付款日期为2020年3月31日,而非2020年3月29日。
(3)付款周期:按期-季,指定首次付款日期,第二次付款的付款日期=首次付款日期中的月份+3,第三次付款的付款日期=首次付款日期中的月份+6,
以此类推;若后续月份中没有首次付款日期中指定的“日”,则默认当月最后一日为计划付款日期;若指定首次付款日期中的“日”为当月最后一天,
则后续月份的计划付款日期也为当月最后一天。
(4)付款周期:按期-半年,指定首次付款日期,第二次付款的付款日期=首次付款日期中的月份+6,第三次付款的付款日期=首次付款
日期中的月份+12,以此类推;若后续月份中没有首次付款日期中指定的“日”,则默认当月最后一日为计划付款日期;
若指定首次付款日期中的“日”为当月最后一天,则后续月份的计划付款日期也为当月最后一天。
(5)付款周期:按期-年,指定首次付款日期,第二次付款的付款日期=首次付款日期中的年份+1,第三次付款的付款日期=首次付款日中的年份+2,
以此类推;若后续月份中没有首次付款日期中指定的“日”,则默认当月最后一日为计划付款日期。若指定首次付款日期中的“日”为当月最后一天,
则后续月份的计划付款日期也为当月最后一天。
3 核心代码
3.1 收付款计划 bean 对象
./src/main/java/com/ljq/demo/bean/PaymentPlanBean.java
package com.ljq.demo.bean;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* @Description: 收付款计划对象
* @Author: junqiang.lu
* @Date: 2021/3/12
*/
@Data
public class PaymentPlanBean implements Serializable {
private static final long serialVersionUID = -1435345954543763628L;
/**
* 收付款计划序号
**/
private Integer sortNo;
/**
* 收付款比例
**/
private BigDecimal paymentRatio;
/**
* 计划收付款金额
**/
private BigDecimal planPayAmt;
/**
* 计划收付款日期
**/
private Date planPayDate;
}
3.2 付款周期类型常量
./src/main/java/com/ljq/demo/constant/PaymentPlanConst.java
package com.ljq.demo.constant;
/**
* @Description: 收付款计划常量
* @Author: junqiang.lu
* @Date: 2021/3/12
*/
public class PaymentPlanConst {
private PaymentPlanConst() {
}
/**
* 收付款周期
* 1: 一次性付清
* 2: 按月计算
* 3: 按季度计算
* 4: 按照半年
* 6: 按年度计算
*/
public static final int PAYMENT_PERIOD_ONCE = 1;
public static final int PAYMENT_PERIOD_MONTH = 2;
public static final int PAYMENT_PERIOD_SEASON = 3;
public static final int PAYMENT_PERIOD_HALF_YEAR = 4;
public static final int PAYMENT_PERIOD_YEAR = 5;
}
3.3 金融金额计算工具类
./src/main/java/com/ljq/demo/util/CalculateUtil.java
package com.ljq.demo.util;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* @Description: 金融计算工具
* @Author: junqiang.lu
* @Date: 2018/7/3
*/
public class CalculateUtil {
/**
* 默认保留小数位数
*/
public static final int DEFAULT_SCALE = 6;
private CalculateUtil(){
}
/**
* 加法计算
*
* @param var1 参数1(加数)
* @param var2 参数2(被加数)
* @param scale 参数精度,并非计算结果精度
* @return 两个参数的和
*/
public static BigDecimal add(double var1, double var2, int scale){
scale = getValidScale(scale);
BigDecimal bigDecimal1 = new BigDecimal(String.valueOf(var1));
BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(var2));
bigDecimal1 = bigDecimal1.setScale(scale, RoundingMode.DOWN);
bigDecimal2 = bigDecimal2.setScale(scale,RoundingMode.DOWN);
return bigDecimal1.add(bigDecimal2);
}
/**
* 减法计算
*
* @param var1 参数1(减数)
* @param var2 参数2(被减数)
* @param scale 参数精度,并非计算结果精度
* @return 两个参数的差
*/
public static BigDecimal subtract(double var1, double var2, int scale){
scale = getValidScale(scale);
BigDecimal bigDecimal1 = new BigDecimal(String.valueOf(var1));
BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(var2));
bigDecimal1 = bigDecimal1.setScale(scale,RoundingMode.DOWN);
bigDecimal2 = bigDecimal2.setScale(scale,RoundingMode.DOWN);
return bigDecimal1.subtract(bigDecimal2);
}
/**
* 乘法计算
*
* @param var1 参数1(乘数)
* @param var2 参数2(被乘数)
* @param scale 参数精度,并非结果精度
* @return 两个参数的乘积
*/
public static BigDecimal multiply(double var1, double var2,int scale){
scale = getValidScale(scale);
BigDecimal bigDecimal1 = new BigDecimal(String.valueOf(var1));
BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(var2));
bigDecimal1 = bigDecimal1.setScale(scale,RoundingMode.DOWN);
bigDecimal2 = bigDecimal2.setScale(scale,RoundingMode.DOWN);
return bigDecimal1.multiply(bigDecimal2);
}
/**
* 乘法计算
*
* @param var1 参数1(乘数)
* @param var2 参数2(被乘数)
* @param scale 参数精度,并非结果精度
* @param resultScale 计算结果精度
* @return 两个参数的乘积
*/
public static BigDecimal multiply(double var1, double var2,int scale, int resultScale){
return multiply(var1, var2, scale).setScale(resultScale, RoundingMode.HALF_DOWN);
}
/**
* 除法计算
*
* @param var1 参数1(除数)
* @param var2 参数2(被除数)
* @param scale 参数精度,并非结果精度
* @return 两个参数的商
*/
public static BigDecimal divide(double var1, double var2,int scale){
scale = getValidScale(scale);
BigDecimal bigDecimal1 = new BigDecimal(String.valueOf(var1));
BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(var2));
bigDecimal1 = bigDecimal1.setScale(scale, RoundingMode.HALF_DOWN);
bigDecimal2 = bigDecimal2.setScale(scale, RoundingMode.HALF_DOWN);
return bigDecimal1.divide(bigDecimal2, scale, RoundingMode.HALF_DOWN);
}
/**
* 除法计算
*
* @param var1 参数1(除数)
* @param var2 参数2(被除数)
* @param scale 参数精度,并非结果精度
* @param resultScale 计算结果精度
* @return 两个参数的商
*/
public static BigDecimal divide(double var1, double var2, int scale, int resultScale){
return divide(var1, var2, scale).setScale(resultScale, RoundingMode.HALF_DOWN);
}
/**
* 获取有效保留小数位数
*
* @param scale 保留小数位数
* @return
*/
private static int getValidScale(int scale) {
scale = Math.abs(scale);
if(scale == 0){
scale = DEFAULT_SCALE;
}
return scale;
}
/**
* 计算次数
*
* @param divisor 除数
* @param dividend 被除数
* @return
*/
public static int getTimes(int divisor, int dividend) {
int reminder = divisor % dividend;
if (reminder == 0) {
return divisor / dividend;
}
return divisor / dividend + 1;
}
}
3.4 金融日期工具类
./src/main/java/com/ljq/demo/util/FinanceDateUtil.java
package com.ljq.demo.util;
import java.util.Calendar;
import java.util.Date;
/**
* @Description: 金融财务日期工具
* @Author: junqiang.lu
* @Date: 2021/3/12
*/
public class FinanceDateUtil {
private FinanceDateUtil(){
}
/**
* 计算两个日期相差的月数
* 当开始日期大于截止日期时,返回 -1
*
* @param startDate 开始日期
* @param endDate 截止日期
* @return
*/
public static int getMonthDifference(Date startDate, Date endDate) {
if (startDate.after(endDate)) {
return -1;
}
Calendar startCalendar = Calendar.getInstance();
startCalendar.setTime(startDate);
Calendar endCalendar = Calendar.getInstance();
endCalendar.setTime(endDate);
Calendar temp = Calendar.getInstance();
temp.setTime(endDate);
temp.add(Calendar.DATE, 1);
int year = endCalendar.get(Calendar.YEAR) - startCalendar.get(Calendar.YEAR);
int month = endCalendar.get(Calendar.MONTH) - startCalendar.get(Calendar.MONTH);
if ((startCalendar.get(Calendar.DATE) == 1) && (temp.get(Calendar.DATE) == 1)) {
return year * 12 + month + 1;
} else if ((startCalendar.get(Calendar.DATE) != 1) && (temp.get(Calendar.DATE) == 1)) {
return year * 12 + month;
} else if ((startCalendar.get(Calendar.DATE) == 1) && (temp.get(Calendar.DATE) != 1)) {
return year * 12 + month;
} else {
return (year * 12 + month - 1) < 0 ? 0 : (year * 12 + month);
}
}
/**
* 获取下一个还款日
*
* @param currentRepaymentDate 当前还款日期
* @param monthInterval 还款间隔月,最少为 1
* @return
*/
private static Date getNextRepaymentDate(Date currentRepaymentDate, int monthInterval) {
monthInterval = monthInterval < 1 ? 1 : monthInterval;
Calendar calendar = Calendar.getInstance();
calendar.setTime(currentRepaymentDate);
calendar.add(Calendar.MONTH, monthInterval);
return calendar.getTime();
}
/**
* 获取下一个还款日
* (包括月最后一天的判断,如果还款日为所在月最后一天,则下一次还款日也是月最后一天)
*
* @param currentRepaymentDate 当前还款日期
* @param monthInterval 还款间隔月,最少为 1
* @param lastDayOfMonth 是否按照月最后一天判断
* @return
*/
public static Date getNextRepaymentDate(Date currentRepaymentDate, int monthInterval, boolean lastDayOfMonth) {
if (!lastDayOfMonth) {
return getNextRepaymentDate(currentRepaymentDate, monthInterval);
}
monthInterval = monthInterval < 1 ? 1 : monthInterval;
Calendar calendar = Calendar.getInstance();
calendar.setTime(currentRepaymentDate);
boolean isLastDayOfMonth = isLastDayOfMonth(calendar);
calendar.add(Calendar.MONTH, monthInterval);
if (isLastDayOfMonth) {
calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
}
return calendar.getTime();
}
/**
* 判断一个日期是否为所在月的最后一天
*
* @param date 日期
* @return
*/
public static boolean isLastDayOfMonth(Date date) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
return isLastDayOfMonth(calendar);
}
/**
* 判断一个日期是否为所在月的最后一天
* @param calendar
* @return
*/
public static boolean isLastDayOfMonth(Calendar calendar) {
return calendar.get(Calendar.DAY_OF_MONTH) == calendar.getActualMaximum(Calendar.DAY_OF_MONTH);
}
}
3.5 财务工具-生成收付款计划
./src/main/java/com/ljq/demo/util/FinanceUtil.java
package com.ljq.demo.util;
import com.ljq.demo.bean.PaymentPlanBean;
import com.ljq.demo.constant.PaymentPlanConst;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Description: 财务工具类
* @Author: junqiang.lu
* @Date: 2021/3/12
*/
public class FinanceUtil {
/**
* 金额计算精度
*/
public static final int AMT_SCALE = 4;
/**
* 比例计算精度
*/
public static final int RATIO_SCALE = 2;
private FinanceUtil() {
}
/**
* 生成收付款计划
*
* @param totalAmt 收付款总金额
* @param period 收付款周期枚举值
* @param startDate 首次收付款日期
* @param endDate 最后一次收付款日期
* @return
*/
public static List<PaymentPlanBean> generatePaymentPlan(BigDecimal totalAmt, int period, Date startDate,
Date endDate) {
// 收付款周期(以月为单位)
int paymentPeriodMonth = convertPaymentPeriod(period);
// 收付款期数
int paymentNumber = getPaymentNumber(paymentPeriodMonth, startDate, endDate);
// 首次付款日是否为所在月最后一天
boolean isLastDayOfMonth = FinanceDateUtil.isLastDayOfMonth(startDate);
// 平均收付款比例
BigDecimal paymentRatio = CalculateUtil.divide(100, paymentNumber, RATIO_SCALE, RATIO_SCALE);
// 平均每次付款金额
BigDecimal perPaymentAmt = CalculateUtil.divide(totalAmt.doubleValue(),paymentNumber, AMT_SCALE, AMT_SCALE);
// 当前还款日期
Date currentRepaymentDate = startDate;
// 生成除最后一期外的收付款数据
List<PaymentPlanBean> paymentPlanList = new ArrayList<>();
PaymentPlanBean paymentPlan;
for (int i = 1; i <= paymentNumber; i++) {
paymentPlan = new PaymentPlanBean();
paymentPlan.setSortNo(i);
paymentPlan.setPaymentRatio(paymentRatio);
paymentPlan.setPlanPayAmt(perPaymentAmt);
if (i > 1) {
currentRepaymentDate = FinanceDateUtil.getNextRepaymentDate(startDate,
(i-1) * paymentPeriodMonth, isLastDayOfMonth);
}
paymentPlan.setPlanPayDate(currentRepaymentDate);
paymentPlanList.add(paymentPlan);
}
// 计算最后一次收付款数据
// 最后一次收付款比例
BigDecimal lastPaymentRatio = CalculateUtil.subtract(100, CalculateUtil
.multiply(paymentRatio.doubleValue(), (paymentNumber -1), RATIO_SCALE).doubleValue(),RATIO_SCALE);
// 最后一次收收付款金额
BigDecimal lastPaymentAmt = CalculateUtil.subtract(totalAmt.doubleValue(), CalculateUtil
.multiply(perPaymentAmt.doubleValue(), (paymentNumber-1),AMT_SCALE).doubleValue(),AMT_SCALE);
// 最后一次还款日期
currentRepaymentDate = currentRepaymentDate.before(endDate) ?
currentRepaymentDate : endDate;
paymentPlanList.get(paymentNumber -1).setPaymentRatio(lastPaymentRatio);
paymentPlanList.get(paymentNumber -1).setPlanPayAmt(lastPaymentAmt);
paymentPlanList.get(paymentNumber -1).setPlanPayDate(currentRepaymentDate);
return paymentPlanList;
}
/**
* 转换付款周期,按月为单位
*
* @param paymentPeriod
* @return
*/
public static int convertPaymentPeriod(int paymentPeriod) {
int defaultMonth = 1;
switch (paymentPeriod) {
case PaymentPlanConst.PAYMENT_PERIOD_ONCE:
return 0;
case PaymentPlanConst.PAYMENT_PERIOD_MONTH:
return defaultMonth;
case PaymentPlanConst.PAYMENT_PERIOD_SEASON:
return 3;
case PaymentPlanConst.PAYMENT_PERIOD_HALF_YEAR:
return 6;
case PaymentPlanConst.PAYMENT_PERIOD_YEAR:
return 12;
default: break;
}
return defaultMonth;
}
/**
* 获取收付款次数
*
* @param paymentPeriodMonth 还款周期(按月为单位)
* @param startDate 首次收付款日期
* @param endDate 最后一次收付款日期
* @return
*/
public static int getPaymentNumber(int paymentPeriodMonth, Date startDate, Date endDate) {
if (paymentPeriodMonth == 0) {
return 1;
}
// 月份间隔
int monthDiff = FinanceDateUtil.getMonthDifference(startDate, endDate) + 1;
// 计算收付款次数
int reminder = monthDiff % paymentPeriodMonth;
if (reminder == 0) {
return monthDiff / paymentPeriodMonth;
}
return (monthDiff / paymentPeriodMonth) + 1;
}
}
3.6 测试
./src/test/java/com/ljq/demo/util/FinanceUtilTest.java
package com.ljq.demo.util;
import cn.hutool.core.date.DateUtil;
import com.ljq.demo.bean.PaymentPlanBean;
import org.junit.Test;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Date;
import java.util.List;
public class FinanceUtilTest {
@Test
public void generatePaymentPlan() {
System.out.println("---------- 测试数据1 ----------");
/**
* 一次性支付
*/
BigDecimal totalAmt = new BigDecimal("8888.8888").setScale(4, RoundingMode.HALF_DOWN);
int period = 1;
Date startDate = DateUtil.parse("2020-01-10");
Date endDate = DateUtil.parse("2021-03-10");
List<PaymentPlanBean> paymentPlanList = FinanceUtil.generatePaymentPlan(totalAmt, period, startDate, endDate);
paymentPlanList.stream().forEach(paymentPlan -> System.out.println(paymentPlan));
System.out.println("---------- 测试数据2 ----------");
/**
* 按月支付
* 起始日期 day 小于等于 结束日期 day;且 起始日期 day < 28(月份最小日数)
*/
totalAmt = new BigDecimal("8888.8888").setScale(4, RoundingMode.HALF_DOWN);
period = 2;
startDate = DateUtil.parse("2020-01-10");
endDate = DateUtil.parse("2021-03-10");
paymentPlanList = FinanceUtil.generatePaymentPlan(totalAmt, period, startDate, endDate);
paymentPlanList.stream().forEach(paymentPlan -> System.out.println(paymentPlan));
System.out.println("---------- 测试数据3 ----------");
/**
* 按月支付
* 起始日期 day 小于等于 结束日期 day;且 起始日期 day 大于 28(最小月份日数)
*/
startDate = DateUtil.parse("2020-01-30");
endDate = DateUtil.parse("2021-03-10");
paymentPlanList = FinanceUtil.generatePaymentPlan(totalAmt, period, startDate, endDate);
paymentPlanList.stream().forEach(paymentPlan -> System.out.println(paymentPlan));
System.out.println("---------- 测试数据4 ----------");
/**
* 按月支付
* 起始日期 day 为月底最后一天
*/
startDate = DateUtil.parse("2020-01-31");
endDate = DateUtil.parse("2021-03-10");
paymentPlanList = FinanceUtil.generatePaymentPlan(totalAmt, period, startDate, endDate);
paymentPlanList.stream().forEach(paymentPlan -> System.out.println(paymentPlan));
System.out.println("---------- 测试数据5 ----------");
/**
* 按季支付
*/
startDate = DateUtil.parse("2020-01-30");
endDate = DateUtil.parse("2021-03-10");
period = 3;
paymentPlanList = FinanceUtil.generatePaymentPlan(totalAmt, period, startDate, endDate);
paymentPlanList.stream().forEach(paymentPlan -> System.out.println(paymentPlan));
System.out.println("---------- 测试数据6 ----------");
/**
* 按半年支付
*/
startDate = DateUtil.parse("2020-01-30");
endDate = DateUtil.parse("2021-03-10");
period = 4;
paymentPlanList = FinanceUtil.generatePaymentPlan(totalAmt, period, startDate, endDate);
paymentPlanList.stream().forEach(paymentPlan -> System.out.println(paymentPlan));
System.out.println("---------- 测试数据7 ----------");
/**
* 按年支付
*/
startDate = DateUtil.parse("2020-01-30");
endDate = DateUtil.parse("2021-03-10");
period = 5;
paymentPlanList = FinanceUtil.generatePaymentPlan(totalAmt, period, startDate, endDate);
paymentPlanList.stream().forEach(paymentPlan -> System.out.println(paymentPlan));
}
}
4 推荐参考资料
5 Github 源码
Gtihub 源码地址 : https://github.com/Flying9001/Demo
个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.