财务金融工具-生成收付款计划

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 推荐参考资料

JAVA获取两个日期间隔几个月

判断某个日期是不是该月的第一天或最后一天

5 Github 源码

Gtihub 源码地址 : https://github.com/Flying9001/Demo

个人公众号:404Code,分享半个互联网人的技术与思考,感兴趣的可以关注.
404Code

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值