Files
gzth/yuthon_report/models/parking_fee_current_month_receivable.py
T
2026-06-22 17:25:54 +08:00

533 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import datetime
import calendar
import logging
from odoo import api, models
from .report_utils import _id_list # partner_id 多选适配工具
_logger = logging.getLogger(__name__)
class ParkingFeeCurrentMonthReceivable(models.Model):
_inherit = 'property.accounts.receivable'
@api.model
def get_parking_fee_current_month_receivable(self, params):
"""
停车费本月应收 = 当期应收 + 前期欠收 - 前期预收
应收类型只取"停车费",其余逻辑同经营收入板块的本月应收
┌──────────────────────────────────────────────────────────┐
│ 当期应收(+):property.accounts.receivable │
│ due_date 在查询月内,management_type = '停车费' │
│ 金额 = original_amount - discount_amount │
├──────────────────────────────────────────────────────────┤
│ 前期欠收(+):两路数据源 │
│ Part1: receivable.modify.line │
│ receivable_id.due_date < 查询月 │
│ receivable_id.management_type = '停车费' │
│ reconciliation_date有值 且 所在月 > due_date月 │
│ 且 >= 查询月 │
│ 金额 = line.amount │
│ Part2: property.accounts.receivable │
│ is_reconciliation=False 且 due_date < 查询月 │
│ 且 payout_date=False 且 management_type='停车费' │
│ 且 id 不在 Part1 已覆盖的 │
│ 金额 = submitted │
├──────────────────────────────────────────────────────────┤
│ 前期预收(-):receivable.modify.line │
│ receivable_id.management_type = '停车费' │
│ reconciliation_date有值 且 < due_date(提前收款) │
│ 且 reconciliation_date < 查询月第一天 │
│ 且不在同一年同一月(跨月) │
│ 金额 = line.amount │
└──────────────────────────────────────────────────────────┘
"""
# ---------- 时间参数 ----------
date_start = params.get("date_start")
date_end = params.get("date_end")
summary_only = params.get("summary_only", False)
if date_start:
query_month_start = datetime.datetime.fromisoformat(date_start).date()
else:
query_month_start = datetime.date.today().replace(day=1)
if date_end:
query_month_end = datetime.datetime.fromisoformat(date_end).date()
else:
# 查询月最后一天
last_day = calendar.monthrange(query_month_start.year, query_month_start.month)[1]
query_month_end = query_month_start.replace(day=last_day)
# ---------- 通用筛选参数 ----------
keyword = params.get("keyword")
condition = params.get('condition')
partner_id = _id_list(params.get('partner_id'))
company_id = _id_list(params.get('company_id'))
property_id = params.get('property_id')
lessee_id = params.get('lessee_id')
contract_id = params.get('contract_id')
# ---------- 停车费类型固定过滤 ----------
# 只取应收类型 = '停车费'
parking_type = self.env['yuthon.management.type'].sudo().search([('name', '=', '停车费')], limit=1)
if not parking_type:
# 没有停车费类型记录,返回0
_logger.warning("[PARKING_FEE] yuthon.management.type 中未找到'停车费'")
return {"rows": [], "stats": [
{'name': '当期应收', 'amount': 0},
{'name': '前期欠收', 'amount': 0},
{'name': '前期预收', 'amount': 0},
{'name': '本月应收', 'amount': 0},
]}
# ================================================================
# (+) 当期应收:property.accounts.receivable
# due_date 在查询月范围内,management_type = 停车费
# ================================================================
receivable_domain = [
('is_invalid', '!=', True),
('due_date', '>=', query_month_start),
('due_date', '<=', query_month_end),
('management_type_id', '=', parking_type.id),
]
# 通用筛选(accounts.receivable 自身字段,无 receivable_id. 前缀)
if keyword:
receivable_domain += [
'|', '|',
('contract_id.contract_code', 'ilike', keyword),
('property_name', 'ilike', keyword),
('lessee_id.name', 'ilike', keyword),
]
if condition:
receivable_domain.append(('tenure_type', '=', condition))
if partner_id:
receivable_domain.append(('precinct_id', 'in', partner_id))
if company_id:
receivable_domain.append(('company_id', 'in', company_id))
if property_id:
receivable_domain.append(('property_ids', 'in', [property_id]))
if lessee_id:
receivable_domain.append(('lessee_id', '=', lessee_id))
if contract_id:
receivable_domain.append(('contract_id', '=', contract_id))
receivables = self.env['property.accounts.receivable'].sudo().search(
receivable_domain, order="due_date asc")
# 当期应收金额 = original_amount - discount_amount
current_receivable_amount = sum(
(rec.original_amount or 0.0) - (rec.discount_amount or 0.0)
for rec in receivables
)
_logger.info("[PARKING_FEE] 当期应收: %d 条, 金额=%.2f", len(receivables), current_receivable_amount)
# ================================================================
# (+) 前期欠收 Part1:receivable.modify.line
# receivable_id.due_date < 查询月 且 management_type = 停车费
# reconciliation_date 有值 且 所在月 > due_date月 且 >= 查询月
# ================================================================
part1_domain = [
('receivable_id.due_date', '<', query_month_start),
('receivable_id.is_invalid', '!=', True),
('receivable_id.management_type_id', '=', parking_type.id),
]
# 通用筛选(receivable_id. 前缀)
if keyword:
part1_domain += [
'|', '|',
('receivable_id.contract_id.contract_code', 'ilike', keyword),
('receivable_id.property_name', 'ilike', keyword),
('receivable_id.lessee_id.name', 'ilike', keyword),
]
if condition:
part1_domain.append(('receivable_id.tenure_type', '=', condition))
if partner_id:
part1_domain.append(('receivable_id.precinct_id', 'in', partner_id))
if company_id:
part1_domain.append(('receivable_id.company_id', 'in', company_id))
if property_id:
part1_domain.append(('receivable_id.property_ids', 'in', [property_id]))
if lessee_id:
part1_domain.append(('receivable_id.lessee_id', '=', lessee_id))
if contract_id:
part1_domain.append(('receivable_id.contract_id', '=', contract_id))
lines = self.env['receivable.modify.line'].sudo().search(
part1_domain, order="receivable_id asc, reconciliation_date asc")
# 内存过滤:reconciliation_date 有值 且 所在月 > due_date月 且 >= 查询月
due_month_start = query_month_start.replace(day=1)
filtered_lines = lines.filtered(
lambda l: l.reconciliation_date and
l.reconciliation_date.replace(day=1) > l.receivable_id.due_date.replace(day=1) and
l.reconciliation_date.replace(day=1) >= due_month_start
)
# 按应收账单聚合,记录已覆盖的 receivable_id(Part2 去重用)
covered_ids = set()
part1_amount = 0.0
# Part1 明细(用于构建 rows)
part1_rec_map = {}
for line in filtered_lines:
rid = line.receivable_id.id
covered_ids.add(rid)
part1_amount += line.amount or 0.0
# 明细聚合
if rid not in part1_rec_map:
part1_rec_map[rid] = {
'rec': line.receivable_id,
'amount': 0.0,
'payout_dates': [],
}
part1_rec_map[rid]['amount'] += line.amount or 0.0
part1_rec_map[rid]['payout_dates'].append(line.reconciliation_date)
_logger.info("[PARKING_FEE] 前期欠收Part1: %d 条modify.line, 金额=%.2f", len(filtered_lines), part1_amount)
# ================================================================
# (+) 前期欠收 Part2:property.accounts.receivable
# is_reconciliation=False 且 due_date < 查询月 且 payout_date=False
# management_type = 停车费 且 id 不在 Part1 的 covered_ids 中
# ================================================================
part2_domain = [
('is_reconciliation', '=', False),
('due_date', '<', query_month_start),
('payout_date', '=', False),
('is_invalid', '!=', True),
('management_type_id', '=', parking_type.id),
]
if covered_ids:
part2_domain.append(('id', 'not in', list(covered_ids)))
# 通用筛选(accounts.receivable 自身字段)
if keyword:
part2_domain += [
'|', '|',
('contract_id.contract_code', 'ilike', keyword),
('property_name', 'ilike', keyword),
('lessee_id.name', 'ilike', keyword),
]
if condition:
part2_domain.append(('tenure_type', '=', condition))
if partner_id:
part2_domain.append(('precinct_id', 'in', partner_id))
if company_id:
part2_domain.append(('company_id', 'in', company_id))
if property_id:
part2_domain.append(('property_ids', 'in', [property_id]))
if lessee_id:
part2_domain.append(('lessee_id', '=', lessee_id))
if contract_id:
part2_domain.append(('contract_id', '=', contract_id))
unreconciled_recs = self.env['property.accounts.receivable'].sudo().search(
part2_domain, order="due_date asc")
# Part2 明细聚合
part2_rec_map = {}
part2_amount = 0.0
for rec in unreconciled_recs:
rid = rec.id
part2_amount += rec.submitted or 0.0
part2_rec_map[rid] = {
'rec': rec,
'amount': rec.submitted or 0.0,
'payout_dates': [],
}
_logger.info("[PARKING_FEE] 前期欠收Part2: %d 条accounts.receivable, 金额=%.2f",
len(unreconciled_recs), part2_amount)
# ================================================================
# (-) 前期预收:receivable.modify.line
# receivable_id.management_type = 停车费
# reconciliation_date有值 且 < due_date(提前收款)
# 且 reconciliation_date < 查询月第一天(前期收的)
# 且不在同一年同一月(跨月)
# ================================================================
pre_advance_domain = [
('reconciliation_date', '!=', False),
('receivable_id.due_date', '>=', query_month_start),
('receivable_id.is_invalid', '!=', True),
('receivable_id.management_type_id', '=', parking_type.id),
]
# 通用筛选(receivable_id. 前缀)
if keyword:
pre_advance_domain += [
'|', '|',
('receivable_id.contract_id.contract_code', 'ilike', keyword),
('receivable_id.property_name', 'ilike', keyword),
('receivable_id.lessee_id.name', 'ilike', keyword),
]
if condition:
pre_advance_domain.append(('receivable_id.tenure_type', '=', condition))
if partner_id:
pre_advance_domain.append(('receivable_id.precinct_id', 'in', partner_id))
if company_id:
pre_advance_domain.append(('receivable_id.company_id', 'in', company_id))
if property_id:
pre_advance_domain.append(('receivable_id.property_ids', 'in', [property_id]))
if lessee_id:
pre_advance_domain.append(('receivable_id.lessee_id', '=', lessee_id))
if contract_id:
pre_advance_domain.append(('receivable_id.contract_id', '=', contract_id))
pre_lines = self.env['receivable.modify.line'].sudo().search(
pre_advance_domain, order="receivable_id asc, reconciliation_date asc")
# 内存过滤:三个条件必须同时满足
pre_advance_amount = 0.0
pre_advance_rec_map = {}
for line in pre_lines:
# 取收款日期(统一转为 date)
pay_date = line.reconciliation_date
if not pay_date:
continue
if hasattr(pay_date, 'date'):
pay_date = pay_date.date()
# 取应收日期
due = line.receivable_id.due_date if line.receivable_id else None
if not due:
continue
# 条件1:提前收款(reconciliation_date < due_date)
if not (pay_date < due):
continue
# 条件2:跨月(不在同一年同一月)
if pay_date.year == due.year and pay_date.month == due.month:
continue
# 条件3:收款在查询月之前(前期预收)
if not (pay_date < query_month_start):
continue
rid = line.receivable_id.id
line_amount = line.amount or 0.0
pre_advance_amount += line_amount
# 明细聚合
if rid not in pre_advance_rec_map:
pre_advance_rec_map[rid] = {
'rec': line.receivable_id,
'amount': 0.0,
'earliest_pay_date': pay_date,
}
pre_advance_rec_map[rid]['amount'] += line_amount
# 取最早收款日期
if pay_date < pre_advance_rec_map[rid]['earliest_pay_date']:
pre_advance_rec_map[rid]['earliest_pay_date'] = pay_date
_logger.info("[PARKING_FEE] 前期预收: 金额=%.2f", pre_advance_amount)
# ================================================================
# 合计:本月应收 = 当期应收 + 前期欠收(Part1+Part2) - 前期预收
# ================================================================
month_receivable = current_receivable_amount + part1_amount + part2_amount - pre_advance_amount
# summary_only:看板只取 stats,不构建 rows
if summary_only:
return {"rows": [], "stats": [
{'name': '当期应收', 'amount': round(current_receivable_amount, 2)},
{'name': '前期欠收', 'amount': round(part1_amount + part2_amount, 2)},
{'name': '前期预收', 'amount': round(pre_advance_amount, 2)},
{'name': '本月应收', 'amount': round(month_receivable, 2)},
]}
# ================================================================
# 构建明细 rows
# ================================================================
rows = []
idx = 1
# 当期应收行
for rec in receivables:
amount = round((rec.original_amount or 0.0) - (rec.discount_amount or 0.0), 2)
contract = rec.contract_id
# 物业穿透
property_val = ''
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
prop = contract.lease_ids[0].address_id
property_val = {
'model': 'yuthon.property',
'id': prop.id,
'display_name': rec.property_name or prop.name,
}
elif rec:
property_val = rec.property_name or ''
rows.append({
"序号": idx,
"类别": "当期应收",
"合同编号": {
'model': 'property.lease.contract',
'id': contract.id,
'display_name': contract.contract_code,
} if contract else "",
"物业名称": property_val,
"承租方名称": {
'model': 'property.tenant.information',
'id': rec.lessee_id.id,
'display_name': rec.lessee_id.name,
} if rec.lessee_id else "",
"应收类型": '停车费',
"应收开始": rec.due_date.strftime('%Y-%m-%d') if rec.due_date else '',
"应收结束": rec.due_date_end.strftime('%Y-%m-%d') if rec.due_date_end else '',
"金额": amount,
"应收金额": amount,
"收款日期": '',
"所属公司": contract.lessor_id.name if contract and contract.lessor_id else "",
"管理员": {
'model': 'res.users',
'id': rec.admin_id.id,
'display_name': rec.admin_id.name,
} if rec.admin_id else "",
})
idx += 1
# 前期欠收 Part1 行(交晚了)
for rid, data in part1_rec_map.items():
rec = data['rec']
amount = round(data['amount'], 2)
contract = rec.contract_id
# 收款日期(多个用逗号分隔)
payout_dates = data.get('payout_dates', [])
payout_date_str = ', '.join(
[d.strftime('%Y-%m-%d') for d in sorted(set(
d.date() if hasattr(d, 'date') else d for d in payout_dates
))]
) if payout_dates else ''
# 物业穿透
property_val = ''
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
prop = contract.lease_ids[0].address_id
property_val = {
'model': 'yuthon.property',
'id': prop.id,
'display_name': rec.property_name or prop.name,
}
elif rec:
property_val = rec.property_name or ''
receivable_amount = round((rec.original_amount or 0.0) - (rec.discount_amount or 0.0), 2)
rows.append({
"序号": idx,
"类别": "前期欠收",
"合同编号": {
'model': 'property.lease.contract',
'id': contract.id,
'display_name': contract.contract_code,
} if contract else "",
"物业名称": property_val,
"承租方名称": {
'model': 'property.tenant.information',
'id': rec.lessee_id.id,
'display_name': rec.lessee_id.name,
} if rec.lessee_id else "",
"应收类型": '停车费',
"应收开始": rec.due_date.strftime('%Y-%m-%d') if rec.due_date else '',
"应收结束": rec.due_date_end.strftime('%Y-%m-%d') if rec.due_date_end else '',
"金额": amount,
"应收金额": receivable_amount,
"收款日期": payout_date_str,
"所属公司": contract.lessor_id.name if contract and contract.lessor_id else "",
"管理员": {
'model': 'res.users',
'id': rec.admin_id.id,
'display_name': rec.admin_id.name,
} if rec.admin_id else "",
})
idx += 1
# 前期欠收 Part2 行(从未收款)
for rid, data in part2_rec_map.items():
rec = data['rec']
amount = round(data['amount'], 2)
contract = rec.contract_id
# 物业穿透
property_val = ''
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
prop = contract.lease_ids[0].address_id
property_val = {
'model': 'yuthon.property',
'id': prop.id,
'display_name': rec.property_name or prop.name,
}
elif rec:
property_val = rec.property_name or ''
receivable_amount = round((rec.original_amount or 0.0) - (rec.discount_amount or 0.0), 2)
rows.append({
"序号": idx,
"类别": "前期欠收",
"合同编号": {
'model': 'property.lease.contract',
'id': contract.id,
'display_name': contract.contract_code,
} if contract else "",
"物业名称": property_val,
"承租方名称": {
'model': 'property.tenant.information',
'id': rec.lessee_id.id,
'display_name': rec.lessee_id.name,
} if rec.lessee_id else "",
"应收类型": '停车费',
"应收开始": rec.due_date.strftime('%Y-%m-%d') if rec.due_date else '',
"应收结束": rec.due_date_end.strftime('%Y-%m-%d') if rec.due_date_end else '',
"金额": amount,
"应收金额": receivable_amount,
"收款日期": '',
"所属公司": contract.lessor_id.name if contract and contract.lessor_id else "",
"管理员": {
'model': 'res.users',
'id': rec.admin_id.id,
'display_name': rec.admin_id.name,
} if rec.admin_id else "",
})
idx += 1
# 前期预收行(提前收款)
for rid, data in pre_advance_rec_map.items():
rec = data['rec']
amount = round(data['amount'], 2)
contract = rec.contract_id
earliest_pay_date = data.get('earliest_pay_date')
# 物业穿透
property_val = ''
if contract and contract.lease_ids and contract.lease_ids[0].address_id:
prop = contract.lease_ids[0].address_id
property_val = {
'model': 'yuthon.property',
'id': prop.id,
'display_name': rec.property_name or prop.name,
}
elif rec:
property_val = rec.property_name or ''
receivable_amount = round((rec.original_amount or 0.0) - (rec.discount_amount or 0.0), 2)
rows.append({
"序号": idx,
"类别": "前期预收",
"合同编号": {
'model': 'property.lease.contract',
'id': contract.id,
'display_name': contract.contract_code,
} if contract else "",
"物业名称": property_val,
"承租方名称": {
'model': 'property.tenant.information',
'id': rec.lessee_id.id,
'display_name': rec.lessee_id.name,
} if rec.lessee_id else "",
"应收类型": '停车费',
"应收开始": rec.due_date.strftime('%Y-%m-%d') if rec.due_date else '',
"应收结束": rec.due_date_end.strftime('%Y-%m-%d') if rec.due_date_end else '',
"金额": -amount, # 预收为负值(减项)
"应收金额": receivable_amount,
"收款日期": earliest_pay_date.strftime('%Y-%m-%d') if earliest_pay_date else '',
"所属公司": contract.lessor_id.name if contract and contract.lessor_id else "",
"管理员": {
'model': 'res.users',
'id': rec.admin_id.id,
'display_name': rec.admin_id.name,
} if rec.admin_id else "",
})
idx += 1
return {
"rows": rows,
"stats": [
{'name': '当期应收', 'amount': round(current_receivable_amount, 2)},
{'name': '前期欠收', 'amount': round(part1_amount + part2_amount, 2)},
{'name': '前期预收', 'amount': round(pre_advance_amount, 2)},
{'name': '本月应收', 'amount': round(month_receivable, 2)},
],
}