XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

STHApplication

【MAUI】寄当家——安卓端交易管理系统

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json;
using System.Globalization;
using Microsoft.Data.Sqlite;

namespace JiDangJia.Core;

public static class RentalInventory
{
    public static List<DbRow> Items(DbRow order)
    {
        try {using var d=JsonDocument.Parse(order.Text("items","[]"));return d.RootElement.EnumerateArray().Select(DbRow.FromJson).ToList();}
        catch(Exception ex)when(ex is JsonException or InvalidOperationException){return [];}
    }
    internal static void Migrate(SqliteConnection c)
    {
        foreach(var row in Database.Query(c,"SELECT * FROM BusinessOrder WHERE kind='RENT'"))
        {
            if(row.Text("deletedAt").Length>0 && row.Text("trashGroup").Length==0)
            {
                row["trashGroup"]="BusinessOrder:"+row.Id;
                Database.Execute(c,"UPDATE BusinessOrder SET trashGroup=@p0 WHERE id=@p1",row.Text("trashGroup"),row.Id);
            }
            var items=Items(row);
            if(items.Count>0 && items.All(i=>i.Text("stockId").Length>0))continue;
            Attach(c,row,items,false);
            Database.Execute(c,"UPDATE BusinessOrder SET items=@p0 WHERE id=@p1",row.Text("items"),row.Id);
        }
    }
    internal static void Attach(SqliteConnection c,DbRow row,List<DbRow> items,bool opening)
    {
        if(items.Count==0)items.Add(new(){["name"]=row.Text("itemName"),["type"]=row.Text("itemType","OTHER"),["images"]=row.Text("images")});
        var seen=new HashSet<string>();
        foreach(var item in items)
        {
            var stockId=item.Text("stockId");
            if(stockId.Length>0)
            {
                if(!seen.Add(stockId))throw new ArgumentException("同一物品不能重复选择");
                var stock=opening?BusinessService.AvailableStock(c,stockId):Database.Query(c,"SELECT * FROM RecycleItem WHERE id=@p0",stockId).FirstOrDefault()??throw new InvalidOperationException("旧租赁物品关联的库存不存在");
                if(stock.Text("source")!="RENTAL")throw new ArgumentException("请选择租赁库存中的物品");
                item.Clear();foreach(var p in stock)item[p.Key]=p.Value;item["stockId"]=stockId;
            }
            else
            {
                var stock=new DbRow(item);
                stockId=Database.Id("stock");stock["id"]=stockId;stock["orderId"]="";stock["source"]="RENTAL";stock["originBusinessId"]=row.Id;
                stock["itemCode"]=item.Text("itemCode").Length>0?item.Text("itemCode"):ItemCatalog.NewCode();
                stock["amount"]=item.Money("cost")>0?item.Money("cost"):item.Money("appraisalPrice");
                stock["costKnown"]=stock.Money("amount")>0?1:0;
                stock["createdAt"]=row.Text("createdAt",Database.Now);stock["status"]="IN_STOCK";
                if(row.Text("deletedAt").Length>0){stock["deletedAt"]=row.Text("deletedAt");stock["trashGroup"]=row.Text("trashGroup","BusinessOrder:"+row.Id);}
                Database.Upsert(c,"RecycleItem",stock);item["stockId"]=stockId;item["itemCode"]=stock.Text("itemCode");item["cost"]=stock.Money("amount");
            }
            if(opening || row.Text("status")=="OPEN")Database.Execute(c,"UPDATE RecycleItem SET status='RENTED',rentalOrderId=@p0 WHERE id=@p1",row.Id,stockId);
        }
        row["items"]=JsonSerializer.Serialize(items);
        row["itemName"]=string.Join("、",items.Select(i=>i.Text("name")));row["itemType"]=items[0].Text("type","OTHER");
    }
    public static string Status(DbRow stock,DbRow? rental,DateTime today)
    {
        if(stock.Text("status")=="RENTED")return rental is not null && IsOverdue(rental,today)?"OVERDUE_RENT":"RENTED";
        return stock.Text("status");
    }
    public static bool IsOverdue(DbRow rental,DateTime today)=>DateTime.TryParse(rental.Text("dueDate"),CultureInfo.InvariantCulture,DateTimeStyles.None,out var due) && due.Date<today.Date;
    internal static void Reconcile(SqliteConnection c)
    {
        foreach(var stock in Database.Query(c,"SELECT * FROM RecycleItem WHERE deletedAt IS NULL"))
        {
            if(stock.Text("status")=="VOID")continue;
            var sale=Database.Query(c,"SELECT id FROM SaleRecord WHERE itemId=@p0 AND status='DONE' AND deletedAt IS NULL LIMIT 1",stock.Id).FirstOrDefault();
            if(sale is not null){Database.Execute(c,"UPDATE RecycleItem SET status='SOLD' WHERE id=@p0",stock.Id);continue;}
            if(stock.Text("source")=="RENTAL")
            {
                var rental=Database.Query(c,"SELECT * FROM BusinessOrder WHERE kind='RENT' AND status='OPEN' AND deletedAt IS NULL ORDER BY createdAt DESC")
                    .FirstOrDefault(r=>Items(r).Any(i=>i.Text("stockId")==stock.Id));
                Database.Execute(c,"UPDATE RecycleItem SET status=@p0,rentalOrderId=@p1,soldAt=NULL WHERE id=@p2",rental is null?(stock.Text("status")=="LISTED"?"LISTED":"IN_STOCK"):"RENTED",rental?.Id,stock.Id);
            }
            else if(stock.Text("status")=="SOLD")Database.Execute(c,"UPDATE RecycleItem SET status='IN_STOCK',soldAt=NULL WHERE id=@p0",stock.Id);
        }
    }
}

public sealed partial class BusinessService
{
    internal static DbRow AvailableStock(SqliteConnection c,string id)
    {
        var stock=Database.Query(c,"SELECT * FROM RecycleItem WHERE id=@p0 AND deletedAt IS NULL AND status IN ('IN_STOCK','LISTED')",id).FirstOrDefault()??throw new InvalidOperationException("物品已售、租赁中或已删除,不能办理此操作");
        if(stock.Text("orderId").Length>0 && Database.Query(c,"SELECT id FROM RecycleOrder WHERE id=@p0 AND status='DONE' AND deletedAt IS NULL",stock.Text("orderId")).Count==0)throw new InvalidOperationException("来源入库单已作废或删除");
        if(stock.Text("source")=="DEAD" && Database.Query(c,"SELECT id FROM PawnTicket WHERE id=@p0 AND deletedAt IS NULL",stock.Text("ticketId")).Count==0)throw new InvalidOperationException("来源当单已删除");
        if(stock.Text("source")=="RENTAL" && stock.Int("costKnown")==0)throw new InvalidOperationException("旧租赁物品缺少购货成本,请在库存详情补录成本后销售");
        return stock;
    }
    private static void ReturnRentalItems(SqliteConnection c,DbRow rental)
    {
        foreach(var item in RentalInventory.Items(rental))if(item.Text("stockId").Length>0)
        {
            if(Database.Query(c,"SELECT id FROM RecycleItem WHERE id=@p0 AND status='RENTED' AND rentalOrderId=@p1 AND deletedAt IS NULL",item.Text("stockId"),rental.Id).Count==0)throw new InvalidOperationException("租赁物品状态已变更,请先核对库存");
            Database.Execute(c,"UPDATE RecycleItem SET status='IN_STOCK',rentalOrderId=NULL WHERE id=@p0",item.Text("stockId"));
        }
    }
    public void LostRental(string id,decimal saleAmount)
    {
        Positive(saleAmount,"设备售价");
        db.Write(c=>
        {
            var row=Database.Query(c,"SELECT * FROM BusinessOrder WHERE id=@p0 AND kind='RENT' AND status='OPEN' AND deletedAt IS NULL",id).FirstOrDefault()??throw new InvalidOperationException("租赁单已经结算");
            if(!RentalInventory.IsOverdue(row,DateTime.Today))throw new InvalidOperationException("租赁尚未逾期,不能按未归还设备销售结算");
            if(saleAmount<row.Money("deposit"))throw new ArgumentException("未归还结算保留全部押金,售价不能低于押金");
            var items=RentalInventory.Items(row);if(items.Count==0)throw new InvalidOperationException("该租赁没有库存明细");
            var stocks=items.Select(i=>Database.Query(c,"SELECT * FROM RecycleItem WHERE id=@p0 AND status='RENTED' AND rentalOrderId=@p1 AND deletedAt IS NULL",i.Text("stockId"),id).FirstOrDefault()??throw new InvalidOperationException("租赁库存状态已变更")).ToList();
            if(stocks.Any(i=>i.Int("costKnown")==0))throw new InvalidOperationException("租赁物品缺少购货成本,请在库存详情补录后按销售结算");
            var allocations=FinanceAnalytics.Split(saleAmount,stocks.Select(i=>i.Money("amount")).ToList());var day=Display.Day(DateTime.Today);
            for(int i=0;i<stocks.Count;i++)
            {
                var stock=stocks[i];var amount=allocations[i];
                Database.Upsert(c,"SaleRecord",new(){["id"]=Database.Id("sale"),["saleNo"]=Number(c,"SaleRecord","saleNo","S"),["itemId"]=stock.Id,["rentalOrderId"]=id,["customerId"]=row.Text("customerId"),["customerName"]=row.Text("customerName"),["amount"]=amount,["cost"]=stock.Money("amount"),["profit"]=Fees.Round(amount-stock.Money("amount")),["saleDate"]=day,["status"]="DONE",["remark"]="租赁未归还,押金抵作售价",["createdAt"]=Database.Now,["updatedAt"]=Database.Now});
                Database.Execute(c,"UPDATE RecycleItem SET status='SOLD',soldAt=@p0 WHERE id=@p1",day,stock.Id);
            }
            // The deposit was received when the rental opened. Only book the new balance now.
            Finance(c,"INCOME","RENT_SALE_BALANCE",saleAmount-row.Money("deposit"),id,day,"未归还设备售价补款(押金已抵扣)");
            Finance(c,"INCOME","RENT_IN",row.Money("amount"),id,day,"未归还租赁租金");
            Database.Execute(c,"UPDATE BusinessOrder SET status='LOST',rentalOutcome='SALE',saleAmount=@p0,endDate=@p1,updatedAt=@p2 WHERE id=@p3",saleAmount,day,Database.Now,id);
            Database.Log(c,"RENT_LOST","BusinessOrder",id,row.Text("orderNo"));return 0;
        });
    }
    public void SetRentalCost(string id,decimal cost)
    {
        if(cost<0 || cost>999999999)throw new ArgumentException("购货成本应在 0 至 999,999,999 之间");
        db.Write(c=>
        {
            var stock=Database.Query(c,"SELECT * FROM RecycleItem WHERE id=@p0 AND source='RENTAL' AND costKnown=0 AND deletedAt IS NULL",id).FirstOrDefault()??throw new InvalidOperationException("仅缺少历史成本的租赁物品可补录");
            if(Database.Query(c,"SELECT id FROM SaleRecord WHERE itemId=@p0 AND status='DONE' AND deletedAt IS NULL",id).Count>0)throw new InvalidOperationException("物品已经销售,不能补录成本");
            Database.Execute(c,"UPDATE RecycleItem SET amount=@p0,costKnown=1 WHERE id=@p1",Fees.Round(cost),id);
            Database.Log(c,"RENT_COST","RecycleItem",id,stock.Text("name"));return 0;
        });
    }
}