Commit bad77032 authored by wenwen.tang's avatar wenwen.tang 😕

feat(房租出单): 本期出单筛选改为按账期区间(起~止)

「房租车位费」本期出单原先按「出单基准日」筛(账单起始日 <= 基准日),
现改为按账期区间筛:命中 = 该行账期起始日落在 [起, 止] 内,区间取代基准日。

- rent_parser: 新增 in_period() / classify_period()(-> 区间内/区间外/不可出单);
  原 classify(rows, on_date) 保留,定时器 due_for_push 仍走它
- service: rent_preview(from, to, all) 改用 classify_period,返回 from/to 取代 on;
  later 语义变为「区间外」;新增 _period_label() 生成账期标签(单月/跨月);
  ingest_rent 增 from_date/to_date 三分支选行,批次记 period_filter_from/to
- server: /api/rent/preview 收 ?from=&to=(?on= 作兼容别名 = to);
  POST /api/rent/batches 透传 from/to
- web: #rp-on 拆为 #rp-from / #rp-to;「显示未到期」-> 「显示区间外」;
  文案改「按账期区间筛选」
- 测试: test_rent 新增 21 项(区间端点/空区间/区间外原因/原 classify 不变/出单/账期标签);
  新增 ui_verify8.py(沙箱+Playwright 端到端 30 项);ui_verify5.py 适配新控件

单测 212 项全绿;ui_verify8 30/30;ui_verify5 43/43。

Co-authored-by: CodeBuddy
parent 422a556c
......@@ -231,6 +231,48 @@ def billable(rows, on_date=None):
return due, later + bad
# ---------------------------------------------------------------- 账期区间筛选
def in_period(row, from_date=None, to_date=None):
"""该行的账期起始日是否落在 [from_date, to_date] 区间内(含端点)。
两侧都可为空 —— 空表示该侧不设限;两侧都空则只要行有账期起始日就算命中。
"""
d = row.get("period_from")
if not d:
return False
f = _d(from_date)
t = _d(to_date)
if f and d < f:
return False
if t and d > t:
return False
return True
def classify_period(rows, from_date=None, to_date=None):
"""按「账期区间」筛选 → (区间内可出单, 区间外可出单, 不可出单)。
命中口径:行的**账期起始日**(台账「账单起始日」)落在 [from_date, to_date] 内。
区间外的行保留在第二桶里(供页面「显示区间外」时列出),带 reasons 说明原因。
"""
hit, outside, bad = [], [], []
for r in rows:
if not r["billable"]:
bad.append(r)
continue
r = dict(r)
if in_period(r, from_date, to_date):
r["due"] = True
r["in_period"] = True
hit.append(r)
else:
r["due"] = False
r["in_period"] = False
r["reasons"] = list(r["reasons"]) + ["账期不在筛选区间内"]
outside.append(r)
return hit, outside, bad
# ---------------------------------------------------------------- 分组
def group_notices(rows):
"""按「出租方 + 租客 + 期间起止」归并成通知书(房间与车位合并一张)。
......
......@@ -23,7 +23,9 @@ Python 标准库 http.server,零外部依赖。启动:
GET /api/wecom/targets 可发送的会话 / 授权人
POST /api/batches 新建一期电费账单(手工录入明细)→ 计费 → 出单
POST /api/rent/batches 新建一期房租车位费账单(按台账出单,自动推进账单起始日)
GET /api/rent/preview 房租车位费本期出单预览(?on=基准日&all=1 含未到期)
GET /api/rent/preview 房租车位费本期出单预览
(?from=账期起&to=账期止&all=1 含区间外;
旧的 ?on=基准日 仍兼容)
GET /api/batches 批次列表 + 按月留档统计(?kind=elec|rent_parking)
GET /api/batches/{id} 批次详情(含明细与单据 + 收款/开票统计)
DELETE /api/batches/{id} 删除批次(body.rollback=true 时回滚账期推进)
......@@ -288,14 +290,17 @@ class Handler(BaseHTTPRequestHandler):
"batches": store.list_batches(kind),
"months": store.batch_months(kind)})
# ---- 房租车位费:本期出单预览
# ---- 房租车位费:本期出单预览(按账期区间筛选)
if path == "/api/rent/preview":
q = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
import datetime as _dt
on = (q.get("on") or [""])[0] or _dt.date.today().strftime("%Y-%m-%d")
f = (q.get("from") or [""])[0].strip()
t = (q.get("to") or [""])[0].strip()
# 兼容旧参数 ?on=基准日 —— 等价于「账期起始日 ≤ 该日」
if not f and not t:
t = (q.get("on") or [""])[0].strip()
show_all = (q.get("all") or ["0"])[0] in ("1", "true", "yes")
return self._send(200, {"ok": True,
**service.rent_preview(on, show_all)})
**service.rent_preview(f, t, show_all)})
# ---- 备注栏图片附件
if path == "/api/attachments":
......@@ -593,6 +598,8 @@ class Handler(BaseHTTPRequestHandler):
username=user["username"],
advance=body.get("advance") is not False,
period_label=body.get("period_label"),
from_date=body.get("from"),
to_date=body.get("to"),
)
return self._send(200, {"ok": True, "batch": batch})
......
......@@ -241,24 +241,26 @@ def ingest_parts(details, period_label, notice_date=None, due_date=None,
# ================================================================ 房租车位费出单
def rent_preview(on_date=None, show_all=False, cfg=None):
def rent_preview(from_date=None, to_date=None, show_all=False, cfg=None):
"""房租车位费的出单预览:台账逐行计费 + 将生成的通知书分组。
on_date 出单基准日:账单起始日 ≤ 该日的行算「到期」
show_all True 时把「未到期」的行也放进可出单列表(供页面「显示未到期」)
from_date/to_date 账期区间(含端点):行的**账期起始日**落在区间内即命中。
两侧可空,空表示该侧不设限。
show_all True 时把「区间外」的可出单行也放进列表(供页面「显示区间外」)
"""
cfg = cfg or store.load_config()
table = tables.load_table(tables.RENT_KEY)
rows = RP.ledger_rows(table)
if not rows:
raise ValueError("「房租车位费」台账尚未就绪。")
due, later, bad = RP.classify(rows, on_date)
ok = due + later if show_all else due
hit, outside, bad = RP.classify_period(rows, from_date, to_date)
ok = hit + outside if show_all else hit
docs = RP.group_notices(ok)
return {
"on": str(on_date or ""),
"from": str(from_date or ""),
"to": str(to_date or ""),
"rows": [RP.strip(r) for r in ok],
"later": [RP.strip(r) for r in later],
"later": [RP.strip(r) for r in outside],
"skipped": [RP.strip(r) for r in bad],
"docs": [{
**RP.strip_doc(d),
......@@ -270,12 +272,31 @@ def rent_preview(on_date=None, show_all=False, cfg=None):
}
def _period_label(picked, fallback):
"""账期标签:取所选行的账期月份。
同一月份的账期(常见情形)→ 「2026年12月」;跨月 → 「2026年12月~2027年2月」;
一行都没有时退回 fallback 所在月份。
"""
months = sorted({(r["period_from"].year, r["period_from"].month)
for r in picked if r.get("period_from")})
if not months:
return f"{fallback.year}年{fallback.month}月"
a = months[0]
if len(months) == 1:
return f"{a[0]}年{a[1]}月"
b = months[-1]
return f"{a[0]}年{a[1]}月~{b[0]}年{b[1]}月"
def ingest_rent(row_ids=None, on_date=None, notice_date=None, due_date=None,
username="admin", advance=True, period_label=None):
username="admin", advance=True, period_label=None,
from_date=None, to_date=None):
"""房租车位费台账 → 出单 → 存档(并推进账单起始日)。
row_ids 给定时只出这些行(以台账为准重算,不信任前端传来的数字);
不给则按 on_date 取「到期」的全部行。
不给则按账期区间 [from_date, to_date] 取全部命中的可出单行(未给区间时
退回旧的「账单起始日 ≤ on_date」口径,供定时器沿用)。
advance=True 时,出单成功后把每行的「账单起始日」推进一个付款周期,
下次再看「本期出单」就只剩新一期。推进是**一次原子写入 + 改动前自动备份**,
......@@ -293,6 +314,10 @@ def ingest_rent(row_ids=None, on_date=None, notice_date=None, due_date=None,
picked = [r for r in rows if r["row_id"] in want and r["billable"]]
if not picked:
raise ValueError("所选行都不满足出单条件(空置 / 每月租金为 0 / 无账单起始日)。")
elif from_date or to_date:
picked, _outside, _bad = RP.classify_period(rows, from_date, to_date)
if not picked:
raise ValueError("账期区间内没有可出单单元:请检查台账的「账单起始日」与所选区间。")
else:
due, _later, _bad = RP.classify(rows, on_date)
picked = due
......@@ -307,7 +332,7 @@ def ingest_rent(row_ids=None, on_date=None, notice_date=None, due_date=None,
dd = _d(due_date) if due_date else month_end(nd)
notice_s = nd.strftime("%Y-%m-%d")
due_s = dd.strftime("%Y-%m-%d")
period_label = str(period_label or "").strip() or f"{day.year}年{day.month}月"
period_label = str(period_label or "").strip() or _period_label(picked, nd)
batch_id = make_batch_id()
el = cfg.get("elec") or {}
......@@ -365,6 +390,8 @@ def ingest_rent(row_ids=None, on_date=None, notice_date=None, due_date=None,
"meter_date": "",
"title": f"{period_label}房租车位费",
"on_date": day.strftime("%Y-%m-%d"),
"period_filter_from": str(from_date or ""),
"period_filter_to": str(to_date or ""),
"notice_date": notice_s,
"due_date": due_s,
"periods": periods,
......
......@@ -165,6 +165,72 @@ class TestClassify(unittest.TestCase):
self.assertEqual(later, [])
# ================================================================ 3b. 账期区间筛选
class TestPeriodFilter(unittest.TestCase):
"""账期区间筛选:命中口径 = 行的账期起始日(AC)落在 [from, to] 内。"""
def setUp(self):
self.rows = [RP._row_of(r) for r in FAKE_LEDGER]
def test_both_ends_means_all_billable(self):
hit, out, bad = RP.classify_period(self.rows, None, None)
self.assertEqual(len(hit) + len(out), 6) # 6 行可出单
self.assertEqual(out, []) # 不限区间 → 无「区间外」
self.assertEqual({r["unit"] for r in bad}, {"301室", "193车位", "306室"})
def test_lower_bound_inclusive(self):
# 账期起 ≥ 2026-11-01 → 只有 302 室(12-01)命中,其余 5 行落在区间外
hit, out, _bad = RP.classify_period(self.rows, "2026-11-01", None)
self.assertEqual({r["unit"] for r in hit}, {"302室"})
self.assertEqual({r["unit"] for r in out},
{"801室", "802室", "190车位", "305室", "129车位"})
def test_upper_bound_inclusive(self):
# 账期起 ≤ 2026-10-31 → 10-01 的 5 行命中,302 室(12-01)落在区间外
hit, out, _bad = RP.classify_period(self.rows, None, "2026-10-31")
self.assertEqual({r["unit"] for r in hit},
{"801室", "802室", "190车位", "305室", "129车位"})
self.assertEqual({r["unit"] for r in out}, {"302室"})
def test_closed_range_hits_boundaries(self):
hit, out, _bad = RP.classify_period(self.rows, "2026-10-01", "2026-12-01")
self.assertEqual(len(hit), 6) # 两端都含
self.assertEqual(out, [])
def test_narrow_range_empty_hit(self):
hit, out, _bad = RP.classify_period(self.rows, "2026-11-01", "2026-11-30")
self.assertEqual(hit, []) # 没有行的账期落在 11 月
self.assertEqual(len(out), 6) # 全部算区间外
def test_outside_rows_carry_reason_and_flag(self):
_hit, out, _bad = RP.classify_period(self.rows, "2027-01-01", None)
self.assertTrue(all(r["in_period"] is False for r in out))
self.assertTrue(all("账期不在筛选区间内" in r["reasons"] for r in out))
def test_hit_rows_flagged_in_period(self):
hit, _out, _bad = RP.classify_period(self.rows, "2026-10-01", None)
self.assertTrue(all(r["in_period"] is True and r["due"] is True for r in hit))
def test_bad_rows_never_in_hit(self):
for f, t in ((None, None), ("2026-01-01", "2027-12-31")):
_hit, _out, bad = RP.classify_period(self.rows, f, t)
self.assertEqual({r["unit"] for r in bad}, {"301室", "193车位", "306室"})
def test_in_period_helper(self):
r = RP._row_of(mkrow("x", bill_start="2026-10-01")) # 账期起 = 2026-10-01
self.assertTrue(RP.in_period(r, "2026-10-01", "2026-10-01"))
self.assertFalse(RP.in_period(r, "2026-10-02", None))
self.assertFalse(RP.in_period(r, None, "2026-09-30"))
self.assertTrue(RP.in_period(r, None, None))
def test_original_classify_untouched(self):
"""账期区间是新增口径,旧的基准日 classify 行为不变(定时器仍用它)。"""
due, later, _bad = RP.classify(self.rows, "2026-10-09")
self.assertEqual({r["unit"] for r in due},
{"801室", "802室", "190车位", "305室", "129车位"})
self.assertEqual({r["unit"] for r in later}, {"302室"})
# ================================================================ 4. 分组
class TestGrouping(unittest.TestCase):
def test_room_and_park_merge_into_one_notice(self):
......@@ -337,6 +403,75 @@ class TestIngest(FakeBackend):
with self.assertRaises(ValueError):
service.ingest_rent(on_date="2020-01-01", notice_date="2020-01-01")
# ---- 账期区间(取代出单基准日)
def test_ingest_by_period_range(self):
b = service.ingest_rent(from_date="2026-10-01", to_date="2026-10-31",
notice_date="2026-10-09", advance=False)
self.assertEqual({d["unit"] for d in b["details"]},
{"801室", "802室", "190车位", "305室", "129车位"})
self.assertEqual(b["period_filter_from"], "2026-10-01")
self.assertEqual(b["period_filter_to"], "2026-10-31")
def test_ingest_period_range_excludes_outside(self):
# 区间只覆盖 12 月 → 只有 302 室命中
b = service.ingest_rent(from_date="2026-12-01", to_date="2026-12-31",
notice_date="2026-10-09", advance=False)
self.assertEqual({d["unit"] for d in b["details"]}, {"302室"})
def test_ingest_period_range_empty_raises(self):
with self.assertRaises(ValueError):
service.ingest_rent(from_date="2020-01-01", to_date="2020-12-31",
notice_date="2020-01-01")
def test_period_label_from_actual_period(self):
# 账期区间落在 10 月 → 标签应取自账期(2026年10月),而不是通知日期
b = service.ingest_rent(from_date="2026-10-01", to_date="2026-10-31",
notice_date="2026-10-09", advance=False)
self.assertEqual(b["period_label"], "2026年10月")
def test_period_label_spans_months(self):
b = service.ingest_rent(from_date="2026-10-01", to_date="2026-12-31",
notice_date="2026-10-09", advance=False)
self.assertEqual(b["period_label"], "2026年10月~2026年12月")
def test_period_label_explicit_wins(self):
b = service.ingest_rent(row_ids=["r3"], notice_date="2026-10-09",
advance=False, period_label="自定义期")
self.assertEqual(b["period_label"], "自定义期")
class TestRentPreview(FakeBackend):
"""service.rent_preview 的账期区间筛选。"""
def test_no_range_lists_all_billable(self):
d = service.rent_preview()
self.assertEqual(len(d["rows"]), 6)
self.assertEqual(d["later"], [])
self.assertEqual(len(d["skipped"]), 3)
def test_lower_bound(self):
d = service.rent_preview(from_date="2026-11-01")
self.assertEqual({r["unit"] for r in d["rows"]}, {"302室"})
self.assertEqual(len(d["later"]), 5)
def test_show_all_includes_outside(self):
d = service.rent_preview(from_date="2026-11-01", show_all=True)
self.assertEqual(len(d["rows"]), 6) # 1 命中 + 5 区间外
self.assertEqual(len(d["later"]), 5)
def test_range_echoed_back(self):
d = service.rent_preview(from_date="2026-10-01", to_date="2026-12-31")
self.assertEqual(d["from"], "2026-10-01")
self.assertEqual(d["to"], "2026-12-31")
self.assertEqual(d["all_count"], 9)
def test_summary_counts(self):
d = service.rent_preview(from_date="2026-10-01", to_date="2026-10-31")
s = d["summary"]
self.assertEqual(s["row_count"], 5)
self.assertEqual(s["skip_count"], 3)
self.assertEqual(s["document_count"], 4)
class TestRollback(FakeBackend):
def test_rolls_back_only_untouched_rows(self):
......
......@@ -5,10 +5,10 @@
覆盖 2026-10-09 交付、2026-10-11 补数据的「房租车位费」出单链路(页面部分):
1. 三张卡齐全(本期出单 / 批次台账 / 批次详情)
2. 默认值:出单基准日 = 今天、通知日期 = 今天、缴费期限 = 当月月底
2. 默认值:账期起/账期止留空(不限区间)、通知日期 = 今天、缴费期限 = 当月月底
3. 预览渲染:汇总条给出「可出单 N 行 / 通知书 M 份 / 合计 ¥X」,明细表行数一致
4. 明细列语义:类型列同时出现「房间」「车位」(**车位参与出单**)
5. 「显示未到期」勾上后行数不少于默认
5. 账期区间:收紧「账期起」后行数不增;勾「显示区间外」补回区间外的行
6. 全选/取消:默认全选;取消后「已勾选」计数归零
7. 只勾 1 行 → 生成出单 → 页面回执 + 批次台账多 1 条 + 批次详情自动展开
8. 出单确实推进了台账「账单起始日」(按付款周期 +N 月)
......@@ -115,10 +115,12 @@ def main():
month_end = js("""() => { const d=new Date(); const z=n=>String(n).padStart(2,'0');
const e=new Date(d.getFullYear(), d.getMonth()+1, 0);
return e.getFullYear()+'-'+z(e.getMonth()+1)+'-'+z(e.getDate()); }""")
ck("基准日默认今天", p.input_value("#rp-on") == today, p.input_value("#rp-on"))
ck("账期起/账期止默认留空(不限区间)",
p.input_value("#rp-from") == "" and p.input_value("#rp-to") == "",
"%r ~ %r" % (p.input_value("#rp-from"), p.input_value("#rp-to")))
ck("通知日期默认今天", p.input_value("#rp-notice") == today, p.input_value("#rp-notice"))
ck("缴费期限默认当月月底", p.input_value("#rp-due") == month_end, p.input_value("#rp-due"))
ck("「显示未到期」默认不勾", not p.is_checked("#rp-showall"))
ck("「显示区间外」默认不勾", not p.is_checked("#rp-showall"))
def pick_rows():
"""真正带勾选框的行(空列表时页面只有 1 行占位 tr)。"""
......@@ -130,18 +132,10 @@ def main():
mark: tr.children[11].innerText.trim()}))""")
ac_all = ledger()
n_default = len(pick_rows())
n_default = len(pick_rows()) # 区间留空 → 全部可出单行
acs = sorted([v for v in ac_all.values() if v])
if n_default == 0:
# 当天没有到期的可出单行(历史批次已把早期账期推进走了,存量行都排在
# 更晚的月份)→ 把基准日推到**最晚**的「账单起始日」,让存量行全部进入
# 列表,这样才能验证链路;跑完必定删除批次并回滚,不留痕。
acs = sorted([v for v in ac_all.values() if v])
if acs:
p.eval_on_selector("#rp-on",
"(e,d)=>{e.value=d;e.dispatchEvent(new Event('change'))}", acs[-1])
p.wait_for_timeout(2400)
n_default = len(pick_rows())
print(" 基准日推至最晚账单起始日 %s(当天没有到期的行)" % acs[-1])
print(" ! 台账当前没有可出单行(空置 / 租金为 0 / 无账单起始日)")
s_default = p.inner_text("#rpSum")
m = re.search(r"可出单\s*(\d+)\s*行\s*/\s*通知书\s*(\d+)\s*份\s*/\s*合计\s*¥([\d,]+)", s_default)
......@@ -173,18 +167,38 @@ def main():
"e=>e.length") == 0)
p.screenshot(path=os.path.join(OUT, "R1-rent-preview.png"), full_page=True)
# ============================================ 3. 显示未到期
print("\n3. 「显示未到期」开关")
p.check("#rp-showall")
p.wait_for_timeout(1800)
n_all = len(pick_rows())
wait_tags = p.eval_on_selector_all("#rpWrap .tag.wait", "e=>e.length")
ck("勾上后行数不少于默认", n_all >= n_default, "%s → %s 行" % (n_default, n_all))
ck("「未到期」标记数 = 勾上后多出来的行数", wait_tags == n_all - n_default,
"标记 %d 行 / 多出 %d 行" % (wait_tags, n_all - n_default))
p.uncheck("#rp-showall")
p.wait_for_timeout(1800)
ck("取消后回到默认行数", len(pick_rows()) == n_default)
# ============================================ 3. 账期区间筛选
print("\n3. 账期区间筛选 · 显示区间外")
if acs and n_default > 0:
# 收紧「账期起」到最晚的账单起始日 → 只剩该期之后的行
latest = acs[-1]
p.eval_on_selector("#rp-from",
"(e,d)=>{e.value=d;e.dispatchEvent(new Event('change'))}", latest)
p.wait_for_timeout(2400)
n_hit = len(pick_rows())
ck("收紧「账期起」后行数不多于不限区间",
n_hit <= n_default, "%s → %s 行" % (n_default, n_hit))
ck("区间内的行都不带「区间外」标记",
p.eval_on_selector_all("#rpWrap .tag.wait", "e=>e.length") == 0)
p.check("#rp-showall")
p.wait_for_timeout(2200)
n_show = len(pick_rows())
tags = p.eval_on_selector_all("#rpWrap .tag.wait", "e=>e.length")
ck("勾「显示区间外」后行数回到全部可出单行",
n_show == n_default, "%s vs %s 行" % (n_show, n_default))
ck("「区间外」标记数 = 多出来的行数",
tags == n_show - n_hit, "标记 %d 行 / 多出 %d 行" % (tags, n_show - n_hit))
p.uncheck("#rp-showall")
p.wait_for_timeout(2200)
ck("取消「显示区间外」后回到区间内行数", len(pick_rows()) == n_hit)
# 清空区间,恢复到默认(不限)
p.eval_on_selector("#rp-from",
"(e)=>{e.value='';e.dispatchEvent(new Event('change'))}")
p.wait_for_timeout(2400)
ck("清空「账期起」后恢复不限区间行数",
len(pick_rows()) == n_default, "%d vs %d" % (len(pick_rows()), n_default))
# ============================================ 4. 勾选
print("\n4. 勾选 · 只选 1 行")
......
This diff is collapsed.
......@@ -199,24 +199,27 @@
<!-- ==================== 房租车位费 ==================== -->
<section class="page" id="page-table">
<div class="card">
<h2>本期出单 <span class="hint">按台账「账单起始日」出《房租车位费收费通知书》</span></h2>
<h2>本期出单 <span class="hint">按「账期区间」筛选台账,出《房租车位费收费通知书》</span></h2>
<div class="desc">
计费口径:<b>房屋租赁费 = 每月租金 × 付款周期</b>;<b>物业费 = 每月物业费 × 付款周期</b>
(8F/11F 不收物业费,台账里那列为空);单元号含「车位」的行出<b>车位租赁费</b>。
账期 = 从台账「账单起始日」起 <b>付款周期</b> 个自然月。
同一租客、同一期间的<b>房间与车位合并到一张通知书</b>(历史口径)。
<br><b>筛选</b>:按「账期起 / 账期止」列出<b>账期起始日落在该区间内</b>的可出单行;两个框都留空即不限区间。
勾上「显示区间外」可把区间外的行也列出来补单。
<br>出单成功后,每行的<b>账单起始日会自动推进一个付款周期</b>,所以这些单元不会再出现在「本期出单」里;
推进前会自动备份台账,删除批次时可一键回滚。空置、每月租金为 0、没填账单起始日的行一律不出单。
</div>
<div class="row" style="align-items:flex-end">
<div style="flex:0 0 160px"><label>出单基准日(只看已到期)</label><input type="date" id="rp-on"></div>
<div style="flex:0 0 150px"><label>账期起(起始日 ≥)</label><input type="date" id="rp-from"></div>
<div style="flex:0 0 150px"><label>账期止(起始日 ≤)</label><input type="date" id="rp-to"></div>
<div style="flex:0 0 150px"><label>通知日期</label><input type="date" id="rp-notice"></div>
<div style="flex:0 0 150px"><label>缴费期限</label><input type="date" id="rp-due"></div>
<div style="flex:0 0 auto">
<label>&nbsp;</label>
<label class="chk lock" title="把「未到账单起始日」的行也列出来,方便补单">
<input type="checkbox" id="rp-showall"> 显示未到期
<label class="chk lock" title="把「账期不在所选区间」的可出单行也列出来,方便补单">
<input type="checkbox" id="rp-showall"> 显示区间外
</label>
</div>
<div style="flex:0 0 auto"><label>&nbsp;</label><button id="btn-rp-refresh">刷新预览</button></div>
......
......@@ -1278,11 +1278,12 @@ function monthEndStr() {
async function loadRentPreview() {
const box = $('#rpWrap');
if (!box) return;
const on = $('#rp-on').value || todayStr();
const from = $('#rp-from').value || '';
const to = $('#rp-to').value || '';
const all = $('#rp-showall').checked ? 1 : 0;
box.innerHTML = '<div class="empty">加载中…</div>';
try {
const d = await api(`/api/rent/preview?on=${encodeURIComponent(on)}&all=${all}`);
const d = await api(`/api/rent/preview?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}&all=${all}`);
RP = d;
RP_PICK = new Set((d.rows || []).map(r => r.row_id)); // 默认全选
renderRentPreview();
......@@ -1321,14 +1322,12 @@ function renderRentPreview() {
</tbody></table></div>`
: '<div class="empty">当前没有可出单的单元</div>';
// ---- 台账明细行(可勾选)
const later = {}; // 未到期的行(勾选框禁用)
(RP.later || []).forEach(r => { later[r.row_id] = true; });
// ---- 台账明细行(可勾选);区间外的行在末列标「区间外」
const body = rows.length
? rows.map(r => {
const on = RP_PICK.has(r.row_id);
const tag = r.due === false ? '<span class="tag wait">未到期</span>' : '';
const tag = (r.in_period === false || r.due === false)
? '<span class="tag wait">区间外</span>' : '';
return `<tr>
<td><input type="checkbox" data-pick="${esc(r.row_id)}"${on ? ' checked' : ''}></td>
<td>${esc(r.owner)}</td>
......@@ -1344,7 +1343,7 @@ function renderRentPreview() {
<td>${tag}</td>
</tr>`;
}).join('')
: '<tr><td colspan="12" class="empty">没有可出单的行</td></tr>';
: '<tr><td colspan="12" class="empty">没有符合账期区间的行</td></tr>';
$('#rpWrap').innerHTML = `<table>
<thead><tr>
......@@ -1373,7 +1372,8 @@ function renderRentPreview() {
function strOf(v) { return v === null || v === undefined ? '' : String(v); }
$('#rp-on').addEventListener('change', loadRentPreview);
$('#rp-from').addEventListener('change', loadRentPreview);
$('#rp-to').addEventListener('change', loadRentPreview);
$('#rp-showall').addEventListener('change', loadRentPreview);
$('#btn-rp-refresh').addEventListener('click', loadRentPreview);
......@@ -1383,11 +1383,13 @@ $('#btn-rp-submit').addEventListener('click', async () => {
if (!ids.length) return toast('请至少勾选一行', 'err');
const un = ids.filter(id => (RP.later || []).some(r => r.row_id === id)).length;
const pf = $('#rp-from').value || '', pt = $('#rp-to').value || '';
const ok = await confirmBox({
title: '确认出单(房租车位费)',
rows: [
{ k: '账期', v: `通知日期 ${esc($('#rp-notice').value || '—')} | 缴费期限 ${esc($('#rp-due').value || '—')}` },
{ k: '行数', v: `<b>${ids.length}</b> 行(其中未到期 ${un} 行)` },
{ k: '账期筛选', v: (pf || pt) ? `${pf || '不限'} ~ ${pt || '不限'}` : '不限区间(全部可出单行)' },
{ k: '单据日期', v: `通知日期 ${esc($('#rp-notice').value || '—')} | 缴费期限 ${esc($('#rp-due').value || '—')}` },
{ k: '行数', v: `<b>${ids.length}</b> 行(其中区间外 ${un} 行)` },
{ k: '合并', v: '同一「出租方 + 租客 + 期间」合并成一张通知书' },
{ k: '账期推进', v: '出单成功后台账「账单起始日」自动 +1 个付款周期(改动前自动备份,可回滚)' },
],
......@@ -1404,7 +1406,8 @@ $('#btn-rp-submit').addEventListener('click', async () => {
method: 'POST',
body: {
row_ids: ids,
on_date: $('#rp-on').value,
from: pf,
to: pt,
notice_date: $('#rp-notice').value,
due_date: $('#rp-due').value,
advance: true,
......@@ -2665,8 +2668,10 @@ function viewerSuffix(name) {
loadUnits();
loadElecDefaults();
loadDirectory(); // 名册候选(台账 / 电费录入的联想都靠它)
// 房租车位费出单的默认日期:基准日与通知日期 = 今天,缴费期限 = 当月最后一天
$('#rp-on').value = todayStr();
// 房租车位费出单的默认日期:账期区间留空(不限,列出全部可出单行),
// 通知日期 = 今天,缴费期限 = 当月最后一天
$('#rp-from').value = '';
$('#rp-to').value = '';
$('#rp-notice').value = todayStr();
$('#rp-due').value = monthEndStr();
const h = hashOf();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment