Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
611b09d57c |
@@ -840,12 +840,14 @@ def _pair_net_change(
|
|||||||
""",
|
""",
|
||||||
params,
|
params,
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
# 全站往来口径:转出(付款方)为正 = 本方应收增加;转入(收款方)为负 = 本方应付增加。
|
||||||
|
# 与 company_transfers.net = outflow − inflow、期初「正数=应收」一致。
|
||||||
total = Decimal("0")
|
total = Decimal("0")
|
||||||
for row in rows:
|
for row in rows:
|
||||||
amount = _parse_decimal(row["amount"])
|
amount = _parse_decimal(row["amount"])
|
||||||
if row["payee_id"] == viewer_id:
|
if row["payer_id"] == viewer_id:
|
||||||
total += amount
|
total += amount
|
||||||
elif row["payer_id"] == viewer_id:
|
elif row["payee_id"] == viewer_id:
|
||||||
total -= amount
|
total -= amount
|
||||||
return total
|
return total
|
||||||
|
|
||||||
|
|||||||
+153
-4
@@ -470,8 +470,54 @@ class BalanceBasisTests(CalculationBase):
|
|||||||
balance = calculation.compute_pair_balance(
|
balance = calculation.compute_pair_balance(
|
||||||
self.connection, self.company_a, self.company_b, cutoff="2026-01-31"
|
self.connection, self.company_a, self.company_b, cutoff="2026-01-31"
|
||||||
)
|
)
|
||||||
self.assertEqual("-80.00", balance["net_change"])
|
# 甲为付款方(转出 80)→ 本期变动为正,表示应收增加
|
||||||
self.assertEqual("-80.00", balance["closing"])
|
self.assertEqual("80.00", balance["net_change"])
|
||||||
|
self.assertEqual("80.00", balance["closing"])
|
||||||
|
|
||||||
|
def test_pair_direction_both_sides_opening_plus_outflow(self) -> None:
|
||||||
|
"""甲应收乙 200 + 甲向乙转出 100 → 甲 +300 / 乙 −300。"""
|
||||||
|
from test_matching import MatchingBase
|
||||||
|
|
||||||
|
item = calculation.create_opening_balance(
|
||||||
|
self.connection, self.company_a, self.company_b, "200", "应收期初", self.admin
|
||||||
|
)
|
||||||
|
calculation.confirm_opening_balance(
|
||||||
|
self.connection, item["id"], "确认", self.admin
|
||||||
|
)
|
||||||
|
helper = object.__new__(MatchingBase)
|
||||||
|
helper.connection = self.connection
|
||||||
|
row_a = helper.add_row(
|
||||||
|
self.company_a,
|
||||||
|
own_account="6222000000000001",
|
||||||
|
cp_account="6222000000000002",
|
||||||
|
expense="100.00",
|
||||||
|
at="2026-01-10T10:00:00",
|
||||||
|
)
|
||||||
|
matching.reconcile_rows(self.connection, [row_a])
|
||||||
|
row_b = helper.add_row(
|
||||||
|
self.company_b,
|
||||||
|
own_account="6222000000000002",
|
||||||
|
cp_account="6222000000000001",
|
||||||
|
income="100.00",
|
||||||
|
at="2026-01-10T11:00:00",
|
||||||
|
)
|
||||||
|
matching.reconcile_rows(self.connection, [row_b])
|
||||||
|
|
||||||
|
from_a = calculation.compute_pair_balance(
|
||||||
|
self.connection, self.company_a, self.company_b, cutoff="2026-01-31"
|
||||||
|
)
|
||||||
|
self.assertEqual("full", from_a["basis"])
|
||||||
|
self.assertEqual("200", from_a["opening"])
|
||||||
|
self.assertEqual("100.00", from_a["net_change"])
|
||||||
|
self.assertEqual("300.00", from_a["closing"])
|
||||||
|
|
||||||
|
from_b = calculation.compute_pair_balance(
|
||||||
|
self.connection, self.company_b, self.company_a, cutoff="2026-01-31"
|
||||||
|
)
|
||||||
|
self.assertEqual("full", from_b["basis"])
|
||||||
|
self.assertEqual("-200", from_b["opening"])
|
||||||
|
self.assertEqual("-100.00", from_b["net_change"])
|
||||||
|
self.assertEqual("-300.00", from_b["closing"])
|
||||||
|
|
||||||
|
|
||||||
class CalculationApiTests(unittest.TestCase):
|
class CalculationApiTests(unittest.TestCase):
|
||||||
@@ -489,11 +535,42 @@ class CalculationApiTests(unittest.TestCase):
|
|||||||
connection, "group-admin", "AdminPass123", "admin",
|
connection, "group-admin", "AdminPass123", "admin",
|
||||||
must_change_password=False,
|
must_change_password=False,
|
||||||
)
|
)
|
||||||
company_id = master_data.create_company(connection, "甲公司", None, None, None)
|
cls.company_a = master_data.create_company(connection, "甲公司", None, None, None)
|
||||||
|
cls.company_b = master_data.create_company(connection, "乙公司", None, None, None)
|
||||||
auth.create_user(
|
auth.create_user(
|
||||||
connection, "cashier-a", "CashierA123", "company", company_id,
|
connection, "cashier-a", "CashierA123", "company", cls.company_a,
|
||||||
must_change_password=False,
|
must_change_password=False,
|
||||||
)
|
)
|
||||||
|
admin = connection.execute(
|
||||||
|
"SELECT * FROM users WHERE username = 'group-admin'"
|
||||||
|
).fetchone()
|
||||||
|
account_a = master_data.submit_bank_account(
|
||||||
|
connection,
|
||||||
|
company_id=cls.company_a,
|
||||||
|
bank_name="中信银行",
|
||||||
|
account_type="基本户",
|
||||||
|
account_number="6222000000000001",
|
||||||
|
start_date="2026-01-01",
|
||||||
|
actor=None,
|
||||||
|
)
|
||||||
|
cls.account_a = master_data.review_bank_account(
|
||||||
|
connection, account_a["id"], "approve", None, admin, effective_from="2026-01-01",
|
||||||
|
)
|
||||||
|
account_b = master_data.submit_bank_account(
|
||||||
|
connection,
|
||||||
|
company_id=cls.company_b,
|
||||||
|
bank_name="中信银行",
|
||||||
|
account_type="基本户",
|
||||||
|
account_number="6222000000000002",
|
||||||
|
start_date="2026-01-01",
|
||||||
|
actor=None,
|
||||||
|
)
|
||||||
|
cls.account_b = master_data.review_bank_account(
|
||||||
|
connection, account_b["id"], "approve", None, admin, effective_from="2026-01-01",
|
||||||
|
)
|
||||||
|
calculation.set_calculation_start_date(
|
||||||
|
connection, "2026-01-01", "初始化", admin
|
||||||
|
)
|
||||||
connection.close()
|
connection.close()
|
||||||
cls.httpd = server.ThreadingHTTPServer(("127.0.0.1", 0), server.AppHandler)
|
cls.httpd = server.ThreadingHTTPServer(("127.0.0.1", 0), server.AppHandler)
|
||||||
cls.port = cls.httpd.server_address[1]
|
cls.port = cls.httpd.server_address[1]
|
||||||
@@ -506,6 +583,78 @@ class CalculationApiTests(unittest.TestCase):
|
|||||||
cls.httpd.shutdown()
|
cls.httpd.shutdown()
|
||||||
cls.temp_dir.cleanup()
|
cls.temp_dir.cleanup()
|
||||||
|
|
||||||
|
def _seed_pair_direction_fixture(self) -> None:
|
||||||
|
"""甲应收乙 200 + 甲转出 100,供 balances/pair 双边断言。"""
|
||||||
|
from test_matching import MatchingBase
|
||||||
|
|
||||||
|
connection = connect(self.db_path)
|
||||||
|
admin = connection.execute(
|
||||||
|
"SELECT * FROM users WHERE username = 'group-admin'"
|
||||||
|
).fetchone()
|
||||||
|
existing = connection.execute(
|
||||||
|
"SELECT 1 FROM opening_balance_revisions LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
if existing is None:
|
||||||
|
item = calculation.create_opening_balance(
|
||||||
|
connection,
|
||||||
|
self.company_a,
|
||||||
|
self.company_b,
|
||||||
|
"200",
|
||||||
|
"应收期初",
|
||||||
|
admin,
|
||||||
|
viewer_company_id=self.company_a,
|
||||||
|
)
|
||||||
|
calculation.confirm_opening_balance(
|
||||||
|
connection, item["id"], "确认", admin
|
||||||
|
)
|
||||||
|
helper = object.__new__(MatchingBase)
|
||||||
|
helper.connection = connection
|
||||||
|
row_a = helper.add_row(
|
||||||
|
self.company_a,
|
||||||
|
own_account="6222000000000001",
|
||||||
|
cp_account="6222000000000002",
|
||||||
|
expense="100.00",
|
||||||
|
at="2026-01-10T10:00:00",
|
||||||
|
)
|
||||||
|
matching.reconcile_rows(connection, [row_a])
|
||||||
|
row_b = helper.add_row(
|
||||||
|
self.company_b,
|
||||||
|
own_account="6222000000000002",
|
||||||
|
cp_account="6222000000000001",
|
||||||
|
income="100.00",
|
||||||
|
at="2026-01-10T11:00:00",
|
||||||
|
)
|
||||||
|
matching.reconcile_rows(connection, [row_b])
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
def test_admin_balances_pair_direction_both_sides(self) -> None:
|
||||||
|
self._seed_pair_direction_fixture()
|
||||||
|
client = Client("127.0.0.1", self.port)
|
||||||
|
client.post_json("/api/login", {
|
||||||
|
"username": "group-admin", "password": "AdminPass123", "portal": "admin",
|
||||||
|
})
|
||||||
|
status, _, body = client.request(
|
||||||
|
"GET",
|
||||||
|
f"/api/admin/balances/pair?from_company_id={self.company_a}"
|
||||||
|
f"&to_company_id={self.company_b}&cutoff=2026-01-31",
|
||||||
|
)
|
||||||
|
self.assertEqual(200, status)
|
||||||
|
bal_a = as_json(body)["balance"]
|
||||||
|
self.assertEqual("200", bal_a["opening"])
|
||||||
|
self.assertEqual("100.00", bal_a["net_change"])
|
||||||
|
self.assertEqual("300.00", bal_a["closing"])
|
||||||
|
|
||||||
|
status, _, body = client.request(
|
||||||
|
"GET",
|
||||||
|
f"/api/admin/balances/pair?from_company_id={self.company_b}"
|
||||||
|
f"&to_company_id={self.company_a}&cutoff=2026-01-31",
|
||||||
|
)
|
||||||
|
self.assertEqual(200, status)
|
||||||
|
bal_b = as_json(body)["balance"]
|
||||||
|
self.assertEqual("-200", bal_b["opening"])
|
||||||
|
self.assertEqual("-100.00", bal_b["net_change"])
|
||||||
|
self.assertEqual("-300.00", bal_b["closing"])
|
||||||
|
|
||||||
def test_company_cannot_call_admin_start_date(self) -> None:
|
def test_company_cannot_call_admin_start_date(self) -> None:
|
||||||
self.client.post_json("/api/login", {
|
self.client.post_json("/api/login", {
|
||||||
"username": "cashier-a", "password": "CashierA123", "portal": "company",
|
"username": "cashier-a", "password": "CashierA123", "portal": "company",
|
||||||
|
|||||||
@@ -133,6 +133,14 @@ class Hel203BrowserSmokeTests(unittest.TestCase):
|
|||||||
cls.company_a,
|
cls.company_a,
|
||||||
must_change_password=False,
|
must_change_password=False,
|
||||||
)
|
)
|
||||||
|
auth.create_user(
|
||||||
|
connection,
|
||||||
|
"cashier-b",
|
||||||
|
CASHIER_PASSWORD,
|
||||||
|
"company",
|
||||||
|
cls.company_b,
|
||||||
|
must_change_password=False,
|
||||||
|
)
|
||||||
admin = connection.execute(
|
admin = connection.execute(
|
||||||
"SELECT * FROM users WHERE username = 'group-admin'"
|
"SELECT * FROM users WHERE username = 'group-admin'"
|
||||||
).fetchone()
|
).fetchone()
|
||||||
@@ -162,7 +170,7 @@ class Hel203BrowserSmokeTests(unittest.TestCase):
|
|||||||
start_date="2026-06-01",
|
start_date="2026-06-01",
|
||||||
actor=None,
|
actor=None,
|
||||||
)
|
)
|
||||||
master_data.review_bank_account(
|
cls.account_b = master_data.review_bank_account(
|
||||||
connection,
|
connection,
|
||||||
account_b["id"],
|
account_b["id"],
|
||||||
"approve",
|
"approve",
|
||||||
@@ -305,7 +313,7 @@ class Hel203BrowserSmokeTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
row_b = _add_row(
|
row_b = _add_row(
|
||||||
cls.company_b,
|
cls.company_b,
|
||||||
account_b["id"],
|
cls.account_b["id"],
|
||||||
"6222000000000002",
|
"6222000000000002",
|
||||||
"6222000000000001",
|
"6222000000000001",
|
||||||
income="100.00",
|
income="100.00",
|
||||||
@@ -418,7 +426,7 @@ class Hel203BrowserSmokeTests(unittest.TestCase):
|
|||||||
timeout=8000,
|
timeout=8000,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 公司端看到完整期末口径
|
# 甲公司端:期初 200 + 转出 100 → 期末 300(应收),与净额卡方向一致
|
||||||
self._login(
|
self._login(
|
||||||
page,
|
page,
|
||||||
portal="company",
|
portal="company",
|
||||||
@@ -429,21 +437,73 @@ class Hel203BrowserSmokeTests(unittest.TestCase):
|
|||||||
page.wait_for_function(
|
page.wait_for_function(
|
||||||
"""() => {
|
"""() => {
|
||||||
const data = document.getElementById('transfersData');
|
const data = document.getElementById('transfersData');
|
||||||
const empty = document.getElementById('transfersEmpty');
|
|
||||||
const ready = (data && !data.hidden) || (empty && !empty.hidden);
|
|
||||||
const card = document.getElementById('tfStatEndingCard');
|
const card = document.getElementById('tfStatEndingCard');
|
||||||
const title = document.getElementById('tfStatNetTitle');
|
return data && !data.hidden && card && !card.hidden;
|
||||||
const emptyHtml = document.getElementById('transfersEmptyStats')?.innerHTML || '';
|
|
||||||
return ready && (
|
|
||||||
(card && !card.hidden) ||
|
|
||||||
(title && title.textContent.includes('期末')) ||
|
|
||||||
emptyHtml.includes('期末')
|
|
||||||
);
|
|
||||||
}""",
|
}""",
|
||||||
timeout=15000,
|
timeout=15000,
|
||||||
)
|
)
|
||||||
|
summary_a = page.evaluate(
|
||||||
|
"""async () => {
|
||||||
|
const r = await fetch('/api/company/intercompany/summary');
|
||||||
|
return r.json();
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
self.assertEqual("ok", summary_a.get("status"))
|
||||||
|
self.assertTrue(summary_a["window"]["has_opening"])
|
||||||
|
self.assertEqual("200.00", summary_a["window"]["opening"])
|
||||||
|
self.assertEqual("300.00", summary_a["window"]["ending"])
|
||||||
|
self.assertEqual("100.00", summary_a["confirmed"]["net_change"])
|
||||||
|
self.assertEqual("receivable", summary_a["confirmed"]["net_direction"])
|
||||||
|
|
||||||
|
opening_text = page.locator("#tfStatOpening").inner_text()
|
||||||
|
ending_text = page.locator("#tfStatEnding").inner_text()
|
||||||
|
net_text = page.locator("#tfStatNet").inner_text()
|
||||||
|
self.assertIn("0.02", opening_text)
|
||||||
|
self.assertIn("0.03", ending_text)
|
||||||
|
self.assertIn("0.01", net_text)
|
||||||
|
self.assertIn("应收", net_text)
|
||||||
|
# 页面不得出现与「正数=应收」矛盾的应付标签套在正净额上
|
||||||
|
self.assertNotRegex(net_text, r"\+.*应付")
|
||||||
|
|
||||||
|
# 乙公司端:期初 −200 + 转入 100 → 期末 −300(应付)
|
||||||
|
self._login(
|
||||||
|
page,
|
||||||
|
portal="company",
|
||||||
|
username="cashier-b",
|
||||||
|
password=CASHIER_PASSWORD,
|
||||||
|
)
|
||||||
|
page.click('a[data-view="transfers"]')
|
||||||
|
page.wait_for_function(
|
||||||
|
"""() => {
|
||||||
|
const data = document.getElementById('transfersData');
|
||||||
|
const card = document.getElementById('tfStatEndingCard');
|
||||||
|
return data && !data.hidden && card && !card.hidden;
|
||||||
|
}""",
|
||||||
|
timeout=15000,
|
||||||
|
)
|
||||||
|
summary_b = page.evaluate(
|
||||||
|
"""async () => {
|
||||||
|
const r = await fetch('/api/company/intercompany/summary');
|
||||||
|
return r.json();
|
||||||
|
}"""
|
||||||
|
)
|
||||||
|
self.assertEqual("ok", summary_b.get("status"))
|
||||||
|
self.assertEqual("-200.00", summary_b["window"]["opening"])
|
||||||
|
self.assertEqual("-300.00", summary_b["window"]["ending"])
|
||||||
|
self.assertEqual("-100.00", summary_b["confirmed"]["net_change"])
|
||||||
|
self.assertEqual("payable", summary_b["confirmed"]["net_direction"])
|
||||||
|
ending_b = page.locator("#tfStatEnding").inner_text()
|
||||||
|
net_b = page.locator("#tfStatNet").inner_text()
|
||||||
|
self.assertIn("0.03", ending_b)
|
||||||
|
self.assertIn("应付", net_b)
|
||||||
|
|
||||||
fatal = [e for e in errors if "openModal is not defined" in e
|
fatal = [e for e in errors if "openModal is not defined" in e
|
||||||
or "Cannot read properties of null" in e]
|
or "Cannot read properties of null" in e
|
||||||
|
or e.startswith("pageerror")
|
||||||
|
or "pageerror" in e.lower()]
|
||||||
|
# page.on('pageerror') 已直接 push 字符串,过滤明显运行时错误
|
||||||
|
runtime = [e for e in errors if not e.startswith("console.")]
|
||||||
|
self.assertEqual([], runtime, runtime)
|
||||||
self.assertEqual([], fatal, fatal)
|
self.assertEqual([], fatal, fatal)
|
||||||
finally:
|
finally:
|
||||||
browser.close()
|
browser.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user