Merge pull request #168 from LQYBill/feat/clientCancelInvoice

feat: client cancel invoice
pull/8523/head
Qiuyi LI 2025-05-23 17:23:12 +02:00 committed by GitHub
commit 9aef46a6cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 121 additions and 21 deletions

View File

@ -11,7 +11,9 @@ SELECT combined.id,
shipping_fee, shipping_fee,
purchase_fee, purchase_fee,
amount, amount,
currency.code AS currency currency.code AS currency,
ordered,
status
FROM ( FROM (
SELECT id, SELECT id,
create_by, create_by,
@ -25,7 +27,9 @@ FROM (
NULL AS shipping_fee, NULL AS shipping_fee,
NULL AS purchase_fee, NULL AS purchase_fee,
amount, amount,
currency_id currency_id,
null AS ordered,
status
FROM credit FROM credit
UNION ALL UNION ALL
SELECT id, SELECT id,
@ -40,7 +44,9 @@ FROM (
total_amount AS shipping_fee, total_amount AS shipping_fee,
pt.total AS purchase_fee, pt.total AS purchase_fee,
COALESCE(pt.total + si.total_amount, si.total_amount) AS amount, COALESCE(pt.total + si.total_amount, si.total_amount) AS amount,
currency_id currency_id,
null AS ordered,
status
FROM shipping_invoice si FROM shipping_invoice si
LEFT JOIN ( LEFT JOIN (
SELECT po.shipping_invoice_number, SUM(poc.purchase_fee) AS total SELECT po.shipping_invoice_number, SUM(poc.purchase_fee) AS total
@ -65,7 +71,9 @@ FROM (
NULL AS shipping_fee, NULL AS shipping_fee,
final_amount AS purchase_fee, final_amount AS purchase_fee,
final_amount AS amount, final_amount AS amount,
currency_id currency_id,
ordered,
status
FROM purchase_order FROM purchase_order
WHERE invoice_number LIKE '%-%-1%' WHERE invoice_number LIKE '%-%-1%'
AND client_id IS NOT NULL AND client_id IS NOT NULL

View File

@ -203,7 +203,7 @@ public class CreditController extends JeecgController<Credit, ICreditService> {
if(!credit.getId().equals(latestCredit.getId())) { if(!credit.getId().equals(latestCredit.getId())) {
return Result.error(409, "Credit cannot be deleted, unless it's the last record."); return Result.error(409, "Credit cannot be deleted, unless it's the last record.");
} }
invoiceService.cancelInvoice(credit.getId(), credit.getInvoiceNumber(), credit.getClientId()); invoiceService.cancelInvoice(credit.getId(), credit.getInvoiceNumber(), credit.getClientId(), true);
return Result.OK("sys.api.entryDeleteSuccess"); return Result.OK("sys.api.entryDeleteSuccess");
} }
@ -340,6 +340,7 @@ public class CreditController extends JeecgController<Credit, ICreditService> {
} }
invoiceDatas.setInvoiceNumber(credit.getInvoiceNumber()); invoiceDatas.setInvoiceNumber(credit.getInvoiceNumber());
invoiceDatas.setDescription(credit.getDescription()); invoiceDatas.setDescription(credit.getDescription());
invoiceDatas.setStatus(credit.getStatus());
return Result.OK(invoiceDatas); return Result.OK(invoiceDatas);
} }
} }

View File

@ -5,15 +5,17 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api; import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpStatus;
import org.jeecg.common.api.vo.Result; import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.query.QueryGenerator; import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.modules.business.entity.Client;
import org.jeecg.modules.business.entity.Invoice; import org.jeecg.modules.business.entity.Invoice;
import org.jeecg.modules.business.service.*; import org.jeecg.modules.business.service.*;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.List; import java.util.*;
@Api(tags = "发票") @Api(tags = "发票")
@RestController @RestController
@ -22,19 +24,59 @@ import java.util.List;
public class InvoiceViewController { public class InvoiceViewController {
@Autowired @Autowired
private InvoiceService invoiceService; private InvoiceService invoiceService;
@Autowired
private IClientService clientService;
@Autowired
private ISecurityService securityService;
@GetMapping(value = "/list") @GetMapping(value = "/list")
public Result<?> list(Invoice invoice, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo, public Result<?> list(Invoice invoice, @RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize, @RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) { HttpServletRequest req) {
QueryWrapper<Invoice> queryWrapper = QueryGenerator.initQueryWrapper(invoice, req.getParameterMap()); boolean isEmployee = securityService.checkIsEmployee();
Map<String, String[]> parameterMap = new HashMap<>(req.getParameterMap());
for(String key : req.getParameterMap().keySet()) {
if(key.equals("clientId") && !isEmployee) {
parameterMap.remove("clientId");
}
if(key.equals("createBy") && !isEmployee) {
parameterMap.remove("createBy");
}
}
if(!isEmployee) {
Client currentClient = clientService.getCurrentClient();
if (currentClient == null) {
return Result.error(HttpStatus.SC_UNAUTHORIZED, "Client is not registered as a user");
}
parameterMap.put("clientId", new String[]{currentClient.getId()});
}
QueryWrapper<Invoice> queryWrapper = QueryGenerator.initQueryWrapper(invoice, parameterMap);
Page<Invoice> page = new Page<>(pageNo, pageSize); Page<Invoice> page = new Page<>(pageNo, pageSize);
IPage<Invoice> pageList = invoiceService.page(page, queryWrapper); IPage<Invoice> pageList = invoiceService.page(page, queryWrapper);
return Result.ok(pageList); return Result.ok(pageList);
} }
@DeleteMapping(value = "/cancelInvoice") @DeleteMapping(value = "/cancelInvoice")
public Result<?> cancelInvoice(@RequestParam("id") String id, @RequestParam("invoiceNumber") String invoiceNumber, @RequestParam("clientId") String clientId) { public Result<?> cancelInvoice(@RequestParam("id") String id, @RequestParam("invoiceNumber") String invoiceNumber, @RequestParam("clientId") String clientId) {
boolean isEmployee = securityService.checkIsEmployee();
Client client = clientService.getById(clientId);
Client currentClient;
if(client == null) {
log.error("Client {} not found", clientId);
return Result.error(HttpStatus.SC_NOT_FOUND, "Client not found");
}
if (!isEmployee) {
currentClient = clientService.getCurrentClient();
if (currentClient == null) {
log.error("Client is not registered as a user : {}", clientId);
return Result.error(HttpStatus.SC_UNAUTHORIZED, "Client is not registered as a user");
}
if(!clientId.equals(currentClient.getId())) {
log.error("Client {} is not authorized to download invoice detail for client {}", currentClient.getInternalCode(), client.getInternalCode());
return Result.error(HttpStatus.SC_NOT_FOUND, "Invoice not found");
}
}
log.info("Cancelling invoice number : {}", invoiceNumber); log.info("Cancelling invoice number : {}", invoiceNumber);
boolean invoiceCancelled = invoiceService.cancelInvoice(id, invoiceNumber, clientId); boolean invoiceCancelled = invoiceService.cancelInvoice(id, invoiceNumber, clientId, isEmployee);
return Result.ok(invoiceCancelled ? "sys.api.invoiceCancelSuccess" : "sys.api.invoiceCancelSuccessFileDeleteFail"); return Result.ok(invoiceCancelled ? "sys.api.invoiceCancelSuccess" : "sys.api.invoiceCancelSuccessFileDeleteFail");
} }
@DeleteMapping(value="/cancelBatchInvoice") @DeleteMapping(value="/cancelBatchInvoice")

View File

@ -328,7 +328,7 @@ public class InvoiceController {
try { try {
List<SkuQuantity> skuQuantities = skuService.getSkuQuantitiesFromOrderIds(param.orderIds()); List<SkuQuantity> skuQuantities = skuService.getSkuQuantitiesFromOrderIds(param.orderIds());
if(skuQuantities.isEmpty()) { if(skuQuantities.isEmpty()) {
return Result.error("Nothing to invoice."); return Result.error(404, "Nothing to invoice.");
} }
String purchaseId = purchaseOrderService.addPurchase(skuQuantities ,param.orderIds()); String purchaseId = purchaseOrderService.addPurchase(skuQuantities ,param.orderIds());
metaData = purchaseOrderService.makeInvoice(purchaseId); metaData = purchaseOrderService.makeInvoice(purchaseId);
@ -1142,6 +1142,7 @@ public class InvoiceController {
invoiceDatas.setInsuranceFee(insuranceFee); invoiceDatas.setInsuranceFee(insuranceFee);
invoiceDatas.setFeeAndQtyPerCountry(feeAndQtyPerCountry); invoiceDatas.setFeeAndQtyPerCountry(feeAndQtyPerCountry);
invoiceDatas.setExtraFees(extraFeesMap); invoiceDatas.setExtraFees(extraFeesMap);
invoiceDatas.setStatus(invoice.getStatus());
return Result.OK(invoiceDatas); return Result.OK(invoiceDatas);
} }
@ -1153,12 +1154,15 @@ public class InvoiceController {
InvoiceDatas invoiceDatas = new InvoiceDatas(); InvoiceDatas invoiceDatas = new InvoiceDatas();
List<PurchaseOrder> invoices = purchaseOrderService.getPurchasesByInvoiceNumber(invoiceNumber); List<PurchaseOrder> invoices = purchaseOrderService.getPurchasesByInvoiceNumber(invoiceNumber);
int invoiceStatus = 1;
if(invoices == null) { if(invoices == null) {
return Result.error("No data for product found."); return Result.error("No data for product found.");
} }
List<PurchaseInvoiceEntry> invoiceData = new ArrayList<>(); List<PurchaseInvoiceEntry> invoiceData = new ArrayList<>();
for(PurchaseOrder order : invoices) { for(PurchaseOrder order : invoices) {
invoiceData.addAll(purchaseOrderContentMapper.selectInvoiceDataByID(order.getId())); invoiceData.addAll(purchaseOrderContentMapper.selectInvoiceDataByID(order.getId()));
if(order.getStatus() == 0)
invoiceStatus = 0;
} }
List<BigDecimal> refundList = iSavRefundService.getRefundAmount(invoiceNumber); List<BigDecimal> refundList = iSavRefundService.getRefundAmount(invoiceNumber);
Map<String, Fee> feeAndQtyPerSku = new HashMap<>(); // it maps number of order and purchase fee per item : <France,<250, 50.30>>, <UK, <10, 2.15>> Map<String, Fee> feeAndQtyPerSku = new HashMap<>(); // it maps number of order and purchase fee per item : <France,<250, 50.30>>, <UK, <10, 2.15>>
@ -1205,7 +1209,7 @@ public class InvoiceController {
invoiceDatas.setRefund(refund); invoiceDatas.setRefund(refund);
invoiceDatas.setFinalAmountEur(invoices.stream().map(PurchaseOrder::getFinalAmount).reduce(BigDecimal.ZERO, BigDecimal::add)); invoiceDatas.setFinalAmountEur(invoices.stream().map(PurchaseOrder::getFinalAmount).reduce(BigDecimal.ZERO, BigDecimal::add));
invoiceDatas.setFeeAndQtyPerSku(feeAndQtyPerSku); invoiceDatas.setFeeAndQtyPerSku(feeAndQtyPerSku);
invoiceDatas.setStatus(invoiceStatus);
return Result.OK(invoiceDatas); return Result.OK(invoiceDatas);
} }
public String countryNameFormatting(String country) { public String countryNameFormatting(String country) {

View File

@ -37,7 +37,8 @@ public class InvoiceDatas {
private BigDecimal finalAmount; private BigDecimal finalAmount;
@JSONField(name = "extraFees") @JSONField(name = "extraFees")
private Map<String, Fee> extraFees; private Map<String, Fee> extraFees;
@JSONField(name = "status")
private Integer status;
} }
@Data @Data
@Builder @Builder

View File

@ -105,4 +105,16 @@ public class Transaction implements Serializable {
@Excel(name = "currency", width = 15) @Excel(name = "currency", width = 15)
@ApiModelProperty(value = "currency") @ApiModelProperty(value = "currency")
private java.lang.String currency; private java.lang.String currency;
/**
* 0: cancelled, 1: normal (default)
*/
@Excel(name = "status", width = 15)
@ApiModelProperty(value = "status")
private java.lang.Integer status;
/**
* 0: not ordered, 1: ordered
*/
@Excel(name = "ordered", width = 15)
@ApiModelProperty(value = "ordered")
private java.lang.Integer ordered;
} }

View File

@ -15,7 +15,7 @@
WHERE invoice_number = #{invoiceNumber} WHERE invoice_number = #{invoiceNumber}
</select> </select>
<select id="fetchShippingInvoice" resultType="org.jeecg.modules.business.entity.ShippingInvoice"> <select id="fetchShippingInvoice" resultType="org.jeecg.modules.business.entity.ShippingInvoice">
SELECT id, total_amount, discount_amount, final_amount, paid_amount, currency_id, client_id, create_time SELECT id, total_amount, discount_amount, final_amount, paid_amount, currency_id, client_id, create_time, status
FROM shipping_invoice s FROM shipping_invoice s
WHERE s.invoice_number = #{invoiceNumber} WHERE s.invoice_number = #{invoiceNumber}
</select> </select>

View File

@ -129,15 +129,17 @@
<select id="getSkuQuantitiesFromOrderIds" resultType="org.jeecg.modules.business.vo.SkuQuantity"> <select id="getSkuQuantitiesFromOrderIds" resultType="org.jeecg.modules.business.vo.SkuQuantity">
SELECT sku_id as ID, sku.erp_code as erp_code, SUM(quantity) AS quantity SELECT sku_id as ID, sku.erp_code as erp_code, SUM(quantity) AS quantity
FROM platform_order_content FROM platform_order_content poc
JOIN sku ON platform_order_content.sku_id = sku.id JOIN sku ON poc.sku_id = sku.id
WHERE platform_order_id IN JOIN platform_order po ON poc.platform_order_id = po.id
WHERE poc.platform_order_id IN
<foreach collection="orderIds" separator="," open="(" close=")" index="index" item="orderId"> <foreach collection="orderIds" separator="," open="(" close=")" index="index" item="orderId">
#{orderId} #{orderId}
</foreach> </foreach>
AND erp_status IN ('1','2','3') AND poc.erp_status IN ('1','2','3')
AND product_available = 0 AND poc.product_available = 0
AND virtual_product_available = 0 AND poc.virtual_product_available = 0
AND po.purchase_invoice_number IS NULL
GROUP BY sku_id; GROUP BY sku_id;
</select> </select>
<select id="countAllSkus" resultType="java.lang.Integer"> <select id="countAllSkus" resultType="java.lang.Integer">

View File

@ -6,7 +6,7 @@ import org.jeecg.modules.business.entity.Invoice;
import java.util.List; import java.util.List;
public interface InvoiceService extends IService<Invoice> { public interface InvoiceService extends IService<Invoice> {
boolean cancelInvoice(String id, String invoiceNumber, String clientId); boolean cancelInvoice(String id, String invoiceNumber, String clientId, boolean isEmployee);
boolean cancelBatchInvoice(List<Invoice> invoices); boolean cancelBatchInvoice(List<Invoice> invoices);
} }

View File

@ -12,6 +12,8 @@ import org.springframework.stereotype.Service;
import java.io.File; import java.io.File;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.LocalDate;
import java.util.Calendar;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -48,6 +50,8 @@ public class InvoiceServiceImpl extends ServiceImpl<InvoiceMapper, Invoice> impl
@Value("${jeecg.path.shippingInvoiceDetailDir}") @Value("${jeecg.path.shippingInvoiceDetailDir}")
private String SHIPPING_INVOICE_DETAIL_LOCATION; private String SHIPPING_INVOICE_DETAIL_LOCATION;
private final int CANCEL_DAYS_LIMIT = 14;
/** /**
* Cancel invoice and deletes generated files. * Cancel invoice and deletes generated files.
* shipping : cancels shipping_invoice by setting status to 0, resets data in platform_order_content, platform_order, sav_refund, and balance * shipping : cancels shipping_invoice by setting status to 0, resets data in platform_order_content, platform_order, sav_refund, and balance
@ -60,7 +64,7 @@ public class InvoiceServiceImpl extends ServiceImpl<InvoiceMapper, Invoice> impl
* @return if invoice is successfully cancelled and files are deleted, will return false even when some files are just missing * @return if invoice is successfully cancelled and files are deleted, will return false even when some files are just missing
*/ */
@Override @Override
public boolean cancelInvoice(String id, String invoiceNumber, String clientId) { public boolean cancelInvoice(String id, String invoiceNumber, String clientId, boolean isEmployee) {
String operationType = Balance.OperationType.DebitCancellation.name(); String operationType = Balance.OperationType.DebitCancellation.name();
String originalOperationType = Balance.OperationType.Debit.name(); String originalOperationType = Balance.OperationType.Debit.name();
BigDecimal amount = BigDecimal.ZERO; BigDecimal amount = BigDecimal.ZERO;
@ -80,6 +84,16 @@ public class InvoiceServiceImpl extends ServiceImpl<InvoiceMapper, Invoice> impl
log.error("Purchase order already cancelled : {}", id); log.error("Purchase order already cancelled : {}", id);
return false; return false;
} }
LocalDate orderDate = po.getCreateTime().toInstant().atZone(Calendar.getInstance().getTimeZone().toZoneId()).toLocalDate();
LocalDate twoWeeksAgo = LocalDate.now().minusDays(CANCEL_DAYS_LIMIT);
if(!isEmployee && orderDate.isBefore(twoWeeksAgo)) {
log.error("Purchase order {} is older than {}, client is not allowed cancel it", invoiceNumber, CANCEL_DAYS_LIMIT);
return false;
}
if(!isEmployee && po.isOrdered()) {
log.error("Purchase order {} is already ordered, cannot be cancelled", invoiceNumber);
return false;
}
currencyId = po.getCurrencyId(); currencyId = po.getCurrencyId();
if (po.getInventoryDocumentString() != null && !po.getInventoryDocumentString().isEmpty()) if (po.getInventoryDocumentString() != null && !po.getInventoryDocumentString().isEmpty())
shippingInvoiceService.deleteAttachmentFile(po.getInventoryDocumentString()); shippingInvoiceService.deleteAttachmentFile(po.getInventoryDocumentString());
@ -99,6 +113,12 @@ public class InvoiceServiceImpl extends ServiceImpl<InvoiceMapper, Invoice> impl
log.error("Shipping invoice already cancelled : {}", id); log.error("Shipping invoice already cancelled : {}", id);
return false; return false;
} }
LocalDate orderDate = si.getCreateTime().toInstant().atZone(Calendar.getInstance().getTimeZone().toZoneId()).toLocalDate();
LocalDate twoWeeksAgo = LocalDate.now().minusDays(CANCEL_DAYS_LIMIT);
if(!isEmployee && orderDate.isBefore(twoWeeksAgo)) {
log.error("Shipping invoice {} is older than {}, client is not allowed to cancel it", invoiceNumber, CANCEL_DAYS_LIMIT);
return false;
}
platformOrderContentService.cancelInvoice(invoiceNumber, clientId); platformOrderContentService.cancelInvoice(invoiceNumber, clientId);
platformOrderService.cancelInvoice(invoiceNumber, clientId); platformOrderService.cancelInvoice(invoiceNumber, clientId);
shippingInvoiceService.cancelInvoice(id); shippingInvoiceService.cancelInvoice(id);
@ -114,7 +134,17 @@ public class InvoiceServiceImpl extends ServiceImpl<InvoiceMapper, Invoice> impl
return false; return false;
} }
if(shippingInvoice.getStatus() == ShippingInvoice.Status.Cancelled.getCode()) { if(shippingInvoice.getStatus() == ShippingInvoice.Status.Cancelled.getCode()) {
log.error("Complete invoice already cancelled : {}", id); log.error("Complete invoice already cancelled : {}", invoiceNumber);
return false;
}
LocalDate orderDate = shippingInvoice.getCreateTime().toInstant().atZone(Calendar.getInstance().getTimeZone().toZoneId()).toLocalDate();
LocalDate twoWeeksAgo = LocalDate.now().minusDays(CANCEL_DAYS_LIMIT);
if(!isEmployee && orderDate.isBefore(twoWeeksAgo)) {
log.error("Complete invoice {} older than {} days, client is not allowed to cancel it", invoiceNumber, CANCEL_DAYS_LIMIT);
return false;
}
if(!isEmployee && purchase.isOrdered()) {
log.error("Purchase order {} for invoice {} is already ordered, cannot be cancelled", id, invoiceNumber);
return false; return false;
} }
if(purchase.getInventoryDocumentString() != null && !purchase.getInventoryDocumentString().isEmpty()) if(purchase.getInventoryDocumentString() != null && !purchase.getInventoryDocumentString().isEmpty())