• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

b1f6c1c4 / ProfessionalAccounting / 324

09 Dec 2024 01:34AM UTC coverage: 54.325% (-0.7%) from 55.047%
324

push

appveyor

b1f6c1c4
pvt prefix

6318 of 11630 relevant lines covered (54.33%)

122.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.47
/AccountingServer.Shell/Facade.cs
1
/* Copyright (C) 2020-2024 b1f6c1c4
2
 *
3
 * This file is part of ProfessionalAccounting.
4
 *
5
 * ProfessionalAccounting is free software: you can redistribute it and/or
6
 * modify it under the terms of the GNU Affero General Public License as
7
 * published by the Free Software Foundation, version 3.
8
 *
9
 * ProfessionalAccounting is distributed in the hope that it will be useful, but
10
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License
12
 * for more details.
13
 *
14
 * You should have received a copy of the GNU Affero General Public License
15
 * along with ProfessionalAccounting.  If not, see
16
 * <https://www.gnu.org/licenses/>.
17
 */
18

19
using System;
20
using System.Collections.Generic;
21
using System.Diagnostics;
22
using System.Linq;
23
using System.Reflection;
24
using System.Threading.Tasks;
25
using AccountingServer.BLL;
26
using AccountingServer.BLL.Util;
27
using AccountingServer.Entities;
28
using AccountingServer.Entities.Util;
29
using AccountingServer.Shell.Carry;
30
using AccountingServer.Shell.Serializer;
31
using AccountingServer.Shell.Util;
32
using static AccountingServer.BLL.Parsing.FacadeF;
33

34
namespace AccountingServer.Shell;
35

36
/// <summary>
37
///     表达式解释器
38
/// </summary>
39
public class Facade
40
{
41
    private readonly AccountingShell m_AccountingShell;
42

43
    /// <summary>
44
    ///     复合表达式解释器
45
    /// </summary>
46
    private readonly ShellComposer m_Composer;
47

48
    private readonly DbSession m_Db;
49

50
    private readonly ExchangeShell m_ExchangeShell;
51

52
    public Facade(string uri = null, string db = null)
10✔
53
    {
10✔
54
        m_Db = new(uri, db);
10✔
55
        m_AccountingShell = new();
10✔
56
        m_ExchangeShell = new();
10✔
57
        m_Composer =
10✔
58
            new()
59
                {
60
                    new CheckShell(),
61
                    new CarryShell(),
62
                    m_ExchangeShell,
63
                    new AssetShell(),
64
                    new AmortizationShell(),
65
                    new PluginShell(),
66
                    m_AccountingShell,
67
                };
68
    }
10✔
69

70
    public Session CreateSession(string user, DateTime dt, string spec = null, int limit = 0)
71
        => new(m_Db, user, dt, spec, limit);
10✔
72

73
    /// <summary>
74
    ///     空记账凭证的表示
75
    /// </summary>
76
    // ReSharper disable once MemberCanBeMadeStatic.Global
77
    public string EmptyVoucher(Session session) => session.Serializer.PresentVoucher(null).Wrap();
1✔
78

79
    /// <summary>
80
    ///     执行表达式
81
    /// </summary>
82
    /// <param name="session">客户端会话</param>
83
    /// <param name="expr">表达式</param>
84
    /// <returns>执行结果</returns>
85
    public IAsyncEnumerable<string> Execute(Session session, string expr)
86
    {
7✔
87
        switch (expr)
7✔
88
        {
89
            case null:
90
            case "":
91
            case " ":
92
                return HelloWorld();
×
93
            case "T":
94
                return ListTitles().ToAsyncEnumerable();
×
95
            case "?":
96
                return ListHelp();
×
97
            case "reload":
98
                return Cfg.ReloadAll();
×
99
            case "die":
100
                Environment.Exit(0);
×
101
                break;
×
102
            case "version":
103
                return ListVersions().ToAsyncEnumerable();
×
104
        }
105

106
        switch (ParsingF.Optional(ref expr, "time ", "slow "))
7✔
107
        {
108
            case "time ":
109
                var repetition = (ulong)ParsingF.DoubleF(ref expr);
×
110
                return ExecuteNormal(session, expr, repetition);
×
111
            case "slow ":
112
                var slow = (int)ParsingF.DoubleF(ref expr);
×
113
                return ExecuteProfile(session, expr, slow);
×
114
            default:
115
                return ExecuteNormal(session, expr, null);
7✔
116
        }
117
    }
7✔
118

119
    private async IAsyncEnumerable<string> ExecuteProfile(Session session, string expr, int slow)
120
    {
×
121
        if (!await m_Db.StartProfiler(slow))
×
122
            throw new ApplicationException("Failed to start profiler");
×
123

124
        try
125
        {
×
126
            await foreach (var s in m_Composer.Execute(expr, session));
×
127
            await foreach (var p in m_Db.StopProfiler())
×
128
                yield return p;
×
129
        }
×
130
        finally
131
        {
×
132
            m_Db.StopProfiler();
×
133
        }
×
134
    }
×
135

136
    private async IAsyncEnumerable<string> ExecuteNormal(Session session, string expr, ulong? repetition)
137
    {
7✔
138
        var sw = new Stopwatch();
7✔
139
        var output = (repetition ?? 1) == 1;
7✔
140

141
        for (var i = 0UL; i < (repetition ?? 1UL); i++)
12✔
142
        {
7✔
143
            sw.Start();
7✔
144
            await foreach (var s in m_Composer.Execute(expr, session))
10✔
145
                if (output)
5✔
146
                    yield return s;
5✔
147

148
            sw.Stop();
5✔
149
            if (!output)
5✔
150
                yield return ".";
×
151
        }
5✔
152
        if (!output)
5✔
153
            yield return "\n";
×
154

155
        if (repetition.HasValue)
5✔
156
            yield return $"time = {(double)sw.ElapsedMilliseconds / repetition.Value}ms\n";
×
157
    }
5✔
158

159
    /// <summary>
160
    ///     执行基础表达式
161
    /// </summary>
162
    /// <param name="session">客户端会话</param>
163
    /// <param name="expr">表达式</param>
164
    /// <returns>执行结果</returns>
165
    public IAsyncEnumerable<string> SafeExecute(Session session, string expr)
166
    {
×
167
        switch (expr)
×
168
        {
169
            case null:
170
            case "":
171
            case " ":
172
                return HelloWorld();
×
173
            case "T":
174
                return ListTitles().ToAsyncEnumerable();
×
175
            case "?":
176
                return ListHelp();
×
177
            case "version":
178
                return ListVersions().ToAsyncEnumerable();
×
179
        }
180

181
        return m_AccountingShell.Execute(expr, session);
×
182
    }
×
183

184
    private static async IAsyncEnumerable<string> HelloWorld()
185
    {
×
186
        const string str = "Hello, World!\n";
187
        foreach (var ch in str)
×
188
        {
×
189
            await Task.Delay(250);
×
190
            yield return $"{ch}";
×
191
        }
×
192
    }
×
193

194
    #region Exchange
195

196
    // ReSharper disable once UnusedMember.Global
197
    public void EnableTimer() => m_ExchangeShell.EnableTimer(m_Db);
×
198

199
    // ReSharper disable once UnusedMember.Global
200
    public async Task ImmediateExchange(Action<string, bool> logger)
201
    {
×
202
        m_Db.ExchangeLogger = logger;
×
203
        await m_ExchangeShell.ImmediateExchange(m_Db);
×
204
        m_Db.ExchangeLogger = null;
×
205
    }
×
206

207
    #endregion
208

209
    #region Miscellaneous
210

211
    /// <summary>
212
    ///     显示控制台帮助
213
    /// </summary>
214
    /// <returns>帮助内容</returns>
215
    private static IAsyncEnumerable<string> ListHelp()
216
        => ResourceHelper.ReadResource("AccountingServer.Shell.Resources.Document.txt", typeof(Facade));
×
217

218
    /// <summary>
219
    ///     显示所有会计科目及其编号
220
    /// </summary>
221
    /// <returns>会计科目及其编号</returns>
222
    private static IEnumerable<string> ListTitles()
223
    {
×
224
        foreach (var title in TitleManager.Titles)
×
225
        {
×
226
            yield return $"{title.Id.AsTitle(),-9}{title.Name}\n";
×
227
            foreach (var subTitle in title.SubTitles)
×
228
                yield return $"{title.Id.AsTitle()}{subTitle.Id.AsSubTitle(),-4}{subTitle.Name}\n";
×
229
        }
×
230
    }
×
231

232
    /// <summary>
233
    ///     显示控制台帮助
234
    /// </summary>
235
    /// <returns>帮助内容</returns>
236
    private static IEnumerable<string> ListVersions()
237
        => AppDomain.CurrentDomain.GetAssemblies().Select(static asm =>
×
238
            {
×
239
                var nm = asm.GetName();
×
240
                var iv = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
×
241
                return $"{nm.Name}@{nm.Version}@{iv}\n";
×
242
            }).OrderBy(static s => s);
×
243

244
    #endregion
245

246
    #region Upsert
247

248
    /// <summary>
249
    ///     更新或添加记账凭证
250
    /// </summary>
251
    /// <param name="session">客户端会话</param>
252
    /// <param name="str">记账凭证的表达式</param>
253
    /// <returns>新记账凭证的表达式</returns>
254
    public async ValueTask<string> ExecuteVoucherUpsert(Session session, string str)
255
    {
10✔
256
        var voucher = session.Serializer.ParseVoucher(str);
10✔
257
        var details = voucher.Details.Where(static d => !d.Currency.EndsWith('#')).ToList();
10✔
258
        var grpCs = details.GroupBy(static d => d.Currency ?? BaseCurrency.Now).ToList();
10✔
259
        var grpUs = details.GroupBy(d => d.User ?? session.Client.User).ToList();
10✔
260
        foreach (var grpC in grpCs)
20✔
261
        {
10✔
262
            var cnt = grpC.Count(static d => !d.Fund.HasValue);
10✔
263
            if (cnt == 1)
10✔
264
                grpC.Single(static d => !d.Fund.HasValue).Fund = -grpC.Sum(static d => d.Fund ?? 0D);
10✔
265
            else if (cnt > 1)
×
266
                foreach (var grpU in grpC.GroupBy(static d => d.User))
×
267
                {
×
268
                    var uunc = grpU.SingleOrDefault(static d => !d.Fund.HasValue);
×
269
                    if (uunc != null)
×
270
                        uunc.Fund = -grpU.Sum(static d => d.Fund ?? 0D);
×
271
                }
×
272
        }
10✔
273

274
        // Automatically append T3998 and T3999 entries
275
        if (grpCs.Count == 1 && grpUs.Count == 1) // single currency, single users: nothing
10✔
276
        {
×
277
            // do nothing
278
        }
×
279
        else if (grpCs.Count == 1 && grpUs.Count > 1) // single currency, multiple users: T3998
10✔
280
            foreach (var grpU in grpUs)
30✔
281
            {
20✔
282
                var sum = grpU.Sum(static d => d.Fund!.Value);
20✔
283
                if (sum.IsZero())
20✔
284
                    continue;
×
285

286
                voucher.Details.Add(
20✔
287
                    new() { User = grpU.Key, Currency = grpCs.First().Key, Title = 3998, Fund = -sum });
288
            }
20✔
289
        else if (grpCs.Count > 1 && grpUs.Count == 1) // single user, multiple currencies: T3999
×
290
            foreach (var grpC in grpCs)
×
291
            {
×
292
                var sum = grpC.Sum(static d => d.Fund!.Value);
×
293
                if (sum.IsZero())
×
294
                    continue;
×
295

296
                voucher.Details.Add(
×
297
                    new() { User = grpUs.First().Key, Currency = grpC.Key, Title = 3999, Fund = -sum });
298
            }
×
299
        else // multiple user, multiple currencies: T3998 on payer, T3998/T3999 on payee
300
        {
×
301
            var grpUCs = details.GroupBy(d => (d.User ?? session.Client.User, d.Currency ?? BaseCurrency.Now))
×
302
                .ToList();
303
            // list of payees (debit)
304
            var ps = grpUCs.Where(static grp => !grp.Sum(static d => d.Fund!.Value).IsNonPositive()).ToList();
×
305
            // list of payers (credit)
306
            var qs = grpUCs.Where(static grp => !grp.Sum(static d => d.Fund!.Value).IsNonNegative()).ToList();
×
307
            // total money received by payees (note: not w.r.t. to currency)
308
            var pss = ps.Sum(static grp => grp.Sum(static d => d.Fund!.Value));
×
309
            // total money sent by payers (note: not w.r.t. to currency)
310
            var qss = qs.Sum(static grp => grp.Sum(static d => d.Fund!.Value));
×
311
            var lst = new List<VoucherDetail>();
×
312
            if (ps.Count == 1 && qs.Count >= 1) // one payee, multiple payers
×
313
                foreach (var q in qs)
×
314
                    QuadratureNormalization(lst, ps[0], pss, q, qss);
×
315
            else if (ps.Count >= 1 && qs.Count == 1) // multiple payees, one payer
×
316
                foreach (var p in ps)
×
317
                    QuadratureNormalization(lst, p, pss, qs[0], qss);
×
318
            // For multiple payee + multiple payer case, simply give up
319
            voucher.Details.AddRange(lst.GroupBy(static d => (d.User, d.Currency, d.Title), static (key, res)
×
320
                => new VoucherDetail
×
321
                    {
322
                        User = key.User, Currency = key.Currency, Title = key.Title, Fund = res.Sum(static d => d.Fund),
×
323
                    }).Where(static d => !d.Fund!.Value.IsZero()));
×
324
        }
×
325

326
        if (!await session.Accountant.UpsertAsync(voucher))
10✔
327
            throw new ApplicationException("更新或添加失败");
×
328

329
        return session.Serializer.PresentVoucher(voucher).Wrap();
10✔
330
    }
10✔
331

332
    /// <summary>
333
    ///     When a user (q) pays another user (p) in different money
334
    /// </summary>
335
    /// <param name="details">Target</param>
336
    /// <param name="p">The payee, user and currency</param>
337
    /// <param name="pss">Total money received by payees</param>
338
    /// <param name="q">The payer, user and currency</param>
339
    /// <param name="qss">Total money sent by payers</param>
340
    private static void QuadratureNormalization(ICollection<VoucherDetail> details,
341
        IGrouping<(string, string), VoucherDetail> p, double pss,
342
        IGrouping<(string, string), VoucherDetail> q, double qss)
343
    {
×
344
        var ps0 = p.Sum(static d => d.Fund!.Value); // money received by the payee
×
345
        var qs0 = q.Sum(static d => d.Fund!.Value); // money sent by the payer
×
346
        var ps = ps0 * qs0 / qss; // money received by the payee from that payer, in payee's currency
×
347
        var qs = qs0 * ps0 / pss; // money sent by the payer to that payee, in payer's currency
×
348
        // UpCp <- UpCq <- UqCq
349
        details.Add(new() { User = p.Key.Item1, Currency = p.Key.Item2, Title = 3999, Fund = -ps });
×
350
        details.Add(new() { User = p.Key.Item1, Currency = q.Key.Item2, Title = 3999, Fund = -qs });
×
351
        details.Add(new() { User = p.Key.Item1, Currency = q.Key.Item2, Title = 3998, Fund = +qs });
×
352
        details.Add(new() { User = q.Key.Item1, Currency = q.Key.Item2, Title = 3998, Fund = -qs });
×
353
    }
×
354

355
    /// <summary>
356
    ///     更新或添加资产
357
    /// </summary>
358
    /// <param name="session">客户端会话</param>
359
    /// <param name="str">资产表达式</param>
360
    /// <returns>新资产表达式</returns>
361
    public async ValueTask<string> ExecuteAssetUpsert(Session session, string str)
362
    {
×
363
        var asset = session.Serializer.ParseAsset(str);
×
364

365
        if (!await session.Accountant.UpsertAsync(asset))
×
366
            throw new ApplicationException("更新或添加失败");
×
367

368
        return session.Serializer.PresentAsset(asset).Wrap();
×
369
    }
×
370

371
    /// <summary>
372
    ///     更新或添加摊销
373
    /// </summary>
374
    /// <param name="session">客户端会话</param>
375
    /// <param name="str">摊销表达式</param>
376
    /// <returns>新摊销表达式</returns>
377
    public async ValueTask<string> ExecuteAmortUpsert(Session session, string str)
378
    {
×
379
        var amort = session.Serializer.ParseAmort(str);
×
380

381
        if (!await session.Accountant.UpsertAsync(amort))
×
382
            throw new ApplicationException("更新或添加失败");
×
383

384
        return session.Serializer.PresentAmort(amort).Wrap();
×
385
    }
×
386

387
    #endregion
388

389
    #region Removal
390

391
    /// <summary>
392
    ///     删除记账凭证
393
    /// </summary>
394
    /// <param name="session">客户端会话</param>
395
    /// <param name="str">记账凭证表达式</param>
396
    /// <returns>是否成功</returns>
397
    public async ValueTask<bool> ExecuteVoucherRemoval(Session session, string str)
398
    {
2✔
399
        var voucher = session.Serializer.ParseVoucher(str);
2✔
400

401
        if (voucher.ID == null)
2✔
402
            throw new ApplicationException("编号未知");
×
403

404
        return await session.Accountant.DeleteVoucherAsync(voucher.ID);
2✔
405
    }
2✔
406

407
    /// <summary>
408
    ///     删除资产
409
    /// </summary>
410
    /// <param name="session">客户端会话</param>
411
    /// <param name="str">资产表达式</param>
412
    /// <returns>是否成功</returns>
413
    public async ValueTask<bool> ExecuteAssetRemoval(Session session, string str)
414
    {
×
415
        var asset = session.Serializer.ParseAsset(str);
×
416

417
        if (!asset.ID.HasValue)
×
418
            throw new ApplicationException("编号未知");
×
419

420
        return await session.Accountant.DeleteAssetAsync(asset.ID.Value);
×
421
    }
×
422

423
    /// <summary>
424
    ///     删除摊销
425
    /// </summary>
426
    /// <param name="session">客户端会话</param>
427
    /// <param name="str">摊销表达式</param>
428
    /// <returns>是否成功</returns>
429
    public async ValueTask<bool> ExecuteAmortRemoval(Session session, string str)
430
    {
×
431
        var amort = session.Serializer.ParseAmort(str);
×
432

433
        if (!amort.ID.HasValue)
×
434
            throw new ApplicationException("编号未知");
×
435

436
        return await session.Accountant.DeleteAmortizationAsync(amort.ID.Value);
×
437
    }
×
438

439
    #endregion
440
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc