diff --git a/yuthon_property_approval/models/inherit_yuthon_property.py b/yuthon_property_approval/models/inherit_yuthon_property.py
index f448ea965..36e440598 100644
--- a/yuthon_property_approval/models/inherit_yuthon_property.py
+++ b/yuthon_property_approval/models/inherit_yuthon_property.py
@@ -1,3 +1,4 @@
+import logging
from datetime import datetime, timedelta, date
from odoo.tools.float_utils import float_round
from numpy.ma.core import default_filler
@@ -664,6 +665,7 @@ class PropertyAccountsReceivable(models.Model):
is_refunded = fields.Boolean(string='是否已退款', compute='_compute_is_refunded', store=True)
penalty = fields.Float(string="违约金基数")
penalty_date = fields.Date(string="违约金日期")
+ penalty_ratio_value = fields.Float(string="违约金比例", digits=(16, 6)) # 从合同 percentage_liquidated_damages.name 带入,千分比需6位精度
invoice_type = fields.Selection(
[('ordinary_invoice', '普通发票'), ('vat_invoice', '增值税发票')], string='发票类型', default='ordinary_invoice')
# 报表新增字段
@@ -1165,6 +1167,23 @@ class PropertyAccountsReceivable(models.Model):
'is_reconciliation': True
})
+ def fill_penalty_ratio_value(self):
+ # 服务器动作调用,通过 active_ids 获取选中的记录集
+ records = self.env["property.accounts.receivable"].browse(
+ self._context.get('active_ids', self._context.get('active_id')))
+ # 如果 context 没有 active_ids(比如通过其他方式调用),回退到 self
+ if not records:
+ records = self
+ # 从关联合同 percentage_liquidated_damages.name 读取违约金比例并写入 penalty_ratio_value
+ updated = 0
+ for record in records:
+ if record.contract_id and record.contract_id.percentage_liquidated_damages:
+ record.penalty_ratio_value = float(record.contract_id.percentage_liquidated_damages.name)
+ updated += 1
+ # 调试日志:打印处理了多少条记录
+ _logger = logging.getLogger(__name__)
+ _logger.info('fill_penalty_ratio_value: 处理 %d 条记录,成功写入 %d 条', len(records), updated)
+
def all_penalty_date(self):
receivable_ids= self.env["property.accounts.receivable"].browse(self._context.get('active_ids', self._context.get('active_id')))
for receivable in receivable_ids:
diff --git a/yuthon_property_approval/views/inherit_yuthon_property_views.xml b/yuthon_property_approval/views/inherit_yuthon_property_views.xml
index 2a81f2f2d..cd5c6454c 100644
--- a/yuthon_property_approval/views/inherit_yuthon_property_views.xml
+++ b/yuthon_property_approval/views/inherit_yuthon_property_views.xml
@@ -393,6 +393,7 @@
+
@@ -662,6 +663,13 @@
code
action = model.update_damages()
+
+ 批量写入违约金比例
+
+
+ code
+ action = model.fill_penalty_ratio_value()
+
同步出租方和承租方
diff --git a/yuthon_report/__manifest__.py b/yuthon_report/__manifest__.py
index 4969c8ad6..e8f4c10d2 100644
--- a/yuthon_report/__manifest__.py
+++ b/yuthon_report/__manifest__.py
@@ -26,6 +26,9 @@
'views/yuthon_contract_report_views.xml',
'views/monthly_precollection_report.xml',
'views/management_area_report.xml',
+ 'views/rent_area_report.xml',
+ 'views/leased_area_report.xml',
+ 'views/rental_rate_report.xml',
'views/current_month_received_report.xml',
'views/parking_fee_yearly_report.xml',
'views/current_receivable_property_fee_report.xml',
diff --git a/yuthon_report/models/__init__.py b/yuthon_report/models/__init__.py
index 07014df48..5e7c2d335 100644
--- a/yuthon_report/models/__init__.py
+++ b/yuthon_report/models/__init__.py
@@ -6,6 +6,9 @@ from . import receivables_message_report
from . import yuthon_contract_report
from . import monthly_precollection_report
from . import management_area_report
+from . import rent_area_report
+from . import leased_area_report
+from . import rental_rate_report
from . import current_month_received_report
from . import parking_fee_yearly_report
from . import current_receivable_property_fee_report
diff --git a/yuthon_report/models/deposit_month_received_report.py b/yuthon_report/models/deposit_month_received_report.py
index 1428a256d..53dfb3f82 100644
--- a/yuthon_report/models/deposit_month_received_report.py
+++ b/yuthon_report/models/deposit_month_received_report.py
@@ -17,9 +17,9 @@ class DepositMonthReceivedReport(models.Model):
- 金额:original_amount - discount_amount
- 状态取数逻辑(放宽 domain + 内存过滤):
- 已收(paid):payout_date 存在且在查询月内(month_first <= payout_date <= month_last)
- - 已退(yes):退款日期(has_date) > 查询月末 → 查询时还没退,应包含
- 退款日期(has_date) <= 查询月末 → 已退,不包含
- (已退状态不受 payout_date 约束)
+ - 已退(yes):退款日期(has_date) > 查询月末 且 收款日期(payout_date)在查询月内
+ → 与前期实收互补,前期实收取 payout_date < month_first,
+ 本月实收取 payout_date 在查询月内,避免同一记录出现在两个报表
"""
date_start = params.get("date_start")
query_date = datetime.datetime.fromisoformat(date_start).date() if date_start else datetime.date.today()
@@ -65,12 +65,13 @@ class DepositMonthReceivedReport(models.Model):
# 内存过滤:按状态分别处理
# - 已收(paid):收款日期在查询月内(month_first <= payout_date <= month_last)
- # - 已退(yes):退款日期 > 查询月末(查询时还没退,不受 payout_date 约束)
+ # - 已退(yes):退款日期 > 查询月末 且 收款日期在查询月内(与前期实收互补,避免重复)
lines = lines.filtered(
lambda l: (
l.deposit_state == 'paid' and l.payout_date and month_first <= l.payout_date <= month_last
) or (
l.deposit_state == 'yes' and l.has_date and l.has_date > month_last
+ and l.payout_date and month_first <= l.payout_date <= month_last
)
)
diff --git a/yuthon_report/models/deposit_pre_received_report.py b/yuthon_report/models/deposit_pre_received_report.py
index 5ba33674e..8a004237e 100644
--- a/yuthon_report/models/deposit_pre_received_report.py
+++ b/yuthon_report/models/deposit_pre_received_report.py
@@ -65,12 +65,13 @@ class DepositPreReceivedReport(models.Model):
# 内存过滤:按状态分别处理
# - 已收(paid):收款日期必须存在且 < 查询月初(前期实收 = 查询月之前收到)
- # - 已退(yes):退款日期 > 查询月末(查询时还没退,不受 payout_date 约束)
+ # - 已退(yes):退款日期 > 查询月末 且 收款日期 < 查询月初(查询时还没退,且是前期收的)
lines = lines.filtered(
lambda l: (
l.deposit_state == 'paid' and l.payout_date and l.payout_date < month_first
) or (
l.deposit_state == 'yes' and l.has_date and l.has_date > month_last
+ and l.payout_date and l.payout_date < month_first
)
)
diff --git a/yuthon_report/models/leased_area_report.py b/yuthon_report/models/leased_area_report.py
new file mode 100644
index 000000000..6285a2d52
--- /dev/null
+++ b/yuthon_report/models/leased_area_report.py
@@ -0,0 +1,207 @@
+import datetime
+import logging
+
+from odoo import api, models
+
+_logger = logging.getLogger(__name__)
+
+
+class LeasedAreaReport(models.Model):
+ _name = 'leased.area.report'
+ _description = '已租面积报表'
+ _auto = False
+
+ @api.model
+ def get_leased_area_report(self, params):
+ """已租面积报表数据源。
+
+ 核心逻辑:
+ 1. 数据源为 property.lease.contract.line(物业列表明细行)
+ 2. 日期过滤:截止查询月合同管理状态还在执行中(contact_state='ing')
+ 3. 物业已租面积:contract_line.property_area(从 manage.information.line.rent_area 取的 compute 字段)
+ 4. 合同已租面积:contract.total_area(合同总面积)
+ 5. 产权证号、产权地址从 title.information.line 取
+ 6. 片区管理员从 manage.information.line 取(按查询日期匹配)
+ 7. 支持按物业名称(property_id)、承租方(lessee_id)、合同编号(contract_id)搜索
+ """
+
+ # ========== 一、参数解析 ==========
+ date_start = params.get("date_start")
+ date_end = params.get("date_end")
+ # 单日期模式,优先取 date_start
+ query_date_str = date_start or date_end
+
+ keyword = params.get("keyword")
+ condition = params.get('condition')
+ partner_id = params.get('partner_id')
+ company_id = params.get('company_id')
+ # 前端侧边栏下拉选择器传入的 ID
+ property_id = params.get('property_id')
+ lessee_id = params.get('lessee_id')
+ contract_id = params.get('contract_id')
+
+ # ========== 二、查询日期解析 ==========
+ query_date = None
+ if query_date_str:
+ try:
+ # 截断时间后缀
+ query_date = datetime.datetime.fromisoformat(query_date_str.split(' ')[0]).date()
+ except Exception:
+ _logger.warning("[LEASED_AREA] 日期解析失败: %s", query_date_str)
+
+ # ========== 三、查询合同明细行 ==========
+ # 关联路径:property.lease.contract.line → address_id(yuthon.property) → lease_id(property.lease.contract)
+ # 核心过滤:合同管理状态为执行中
+ domain = [
+ ('address_id', '!=', False),
+ ('lease_id', '!=', False),
+ ('lease_id.contact_state', '=', 'ing'), # 只取执行中的合同
+ ]
+
+ # 承租方过滤
+ if lessee_id:
+ domain.append(('lease_id.lessee_id', '=', int(lessee_id)))
+ # 合同编号过滤
+ if contract_id:
+ domain.append(('lease_id', '=', int(contract_id)))
+ # 物业名称下拉选择器
+ if property_id:
+ domain.append(('address_id', '=', int(property_id)))
+
+ # 查询合同明细行
+ contract_lines = self.env['property.lease.contract.line'].sudo().search(domain)
+
+ # ========== 四、批量预取管理信息(片区管理员) ==========
+ # 需要从 manage.information.line 取片区管理员,按日期匹配
+ property_ids = list({cl.address_id.id for cl in contract_lines if cl.address_id})
+ manage_map = {} # property_id → manage.information.line
+ if property_ids:
+ manage_domain = [('yuthon_property_id', 'in', property_ids)]
+ if query_date:
+ # 按查询日期匹配管理信息行
+ manage_domain += [
+ ('start_time', '<=', query_date),
+ '|', ('end_date', '>=', query_date), ('end_date', '=', False),
+ ]
+ manage_lines = self.env['manage.information.line'].sudo().search(manage_domain)
+ for ml in manage_lines:
+ if ml.yuthon_property_id:
+ # 同一物业取最新的管理信息
+ manage_map[ml.yuthon_property_id.id] = ml
+
+ # ========== 五、构建报表行 ==========
+ rows = []
+ total_property_area = 0.0 # 物业已租面积合计
+ total_contract_area = 0.0 # 合同已租面积合计
+
+ for idx, cl in enumerate(contract_lines, start=1):
+ property_rec = cl.address_id
+ contract = cl.lease_id
+ if not property_rec or not contract:
+ continue
+
+ # --- 5.1 合同信息 ---
+ contract_code = contract.contract_code or ""
+ contract_record_id = contract.id
+ lessee_name = contract.lessee_id.name if contract.lessee_id else ""
+ lessee_record_id = contract.lessee_id.id if contract.lessee_id else False
+
+ # --- 5.2 关键字过滤(物业名称、承租方、合同编号) ---
+ if keyword:
+ match = False
+ kw = keyword.lower()
+ # 物业名称
+ if property_rec.name and kw in property_rec.name.lower():
+ match = True
+ # 承租方
+ if lessee_name and kw in lessee_name.lower():
+ match = True
+ # 合同编号
+ if contract_code and kw in contract_code.lower():
+ match = True
+ if not match:
+ continue
+
+ # --- 5.3 权属类型过滤 ---
+ manage_line = manage_map.get(property_rec.id)
+ if condition:
+ if not manage_line or manage_line.tenure_type != condition:
+ continue
+
+ # --- 5.4 所属片区/公司过滤(通过管理信息) ---
+ if partner_id:
+ if not manage_line or manage_line.precinct_id.id != int(partner_id):
+ continue
+ if company_id:
+ if not manage_line or manage_line.company_id.id != int(company_id):
+ continue
+
+ # --- 5.5 取产权信息(产权证号、产权地址) ---
+ title_certificate = ""
+ property_address = ""
+ if property_rec.title_information_ids:
+ valid_titles = property_rec.title_information_ids
+ if query_date:
+ valid_titles = valid_titles.filtered(
+ lambda t: t.start_time and t.start_time <= query_date and
+ (not t.end_date or t.end_date >= query_date)
+ )
+ if valid_titles:
+ # 按生效日期降序取最新的
+ latest_title = valid_titles.sorted('start_time', reverse=True)[0]
+ title_certificate = latest_title.title_certificate or ""
+ if latest_title.property_message_id:
+ property_address = latest_title.property_message_id.name or ""
+
+ # --- 5.6 片区管理员 ---
+ admin_display = ""
+ if manage_line and manage_line.precinct_id:
+ admin_display = manage_line.precinct_id.display_name or ""
+
+ # --- 5.7 双面积字段 ---
+ # 物业已租面积:从 contract_line.property_area 取(compute 字段,取 address_id 最新管理信息的 rent_area)
+ property_area = cl.property_area or 0.0
+ # 合同已租面积:从 contract.total_area 取
+ contract_area = contract.total_area or 0.0
+
+ # 累计统计
+ total_property_area += property_area
+ total_contract_area += contract_area
+
+ # --- 5.8 构建行数据(物业名称、合同编号、承租方支持穿透) ---
+ rows.append({
+ "序号": idx,
+ "物业id": property_rec.id,
+ "物业名称": {
+ 'model': 'yuthon.property',
+ 'id': property_rec.id,
+ 'display_name': property_rec.name or "",
+ } if property_rec.name else "",
+ "合同编号": {
+ 'model': 'property.lease.contract',
+ 'id': contract_record_id,
+ 'display_name': contract_code,
+ } if contract_record_id else contract_code or "",
+ "承租方": {
+ 'model': 'property.tenant.information',
+ 'id': lessee_record_id,
+ 'display_name': lessee_name,
+ } if lessee_record_id else lessee_name or "",
+ "产权证号": title_certificate,
+ "产权地址": property_address,
+ "物业已租面积": round(property_area, 4),
+ "合同已租面积": round(contract_area, 4),
+ "片区管理员": admin_display,
+ })
+
+ # ========== 六、构建统计汇总数据 ==========
+ stats = [
+ {'name': '记录总数', 'amount': len(rows)},
+ {'name': '物业已租面积合计', 'amount': round(total_property_area, 4)},
+ {'name': '合同已租面积合计', 'amount': round(total_contract_area, 4)},
+ ]
+
+ _logger.info("[LEASED_AREA] query_date=%s, rows=%d, property_area=%.4f, contract_area=%.4f",
+ query_date, len(rows), total_property_area, total_contract_area)
+
+ return {"rows": rows, "stats": stats}
diff --git a/yuthon_report/models/management_area_report.py b/yuthon_report/models/management_area_report.py
index 7107646f8..7fd376345 100644
--- a/yuthon_report/models/management_area_report.py
+++ b/yuthon_report/models/management_area_report.py
@@ -1,7 +1,10 @@
import datetime
+import logging
from odoo import api, models
+_logger = logging.getLogger(__name__)
+
class ManagementAreaReport(models.Model):
_name = 'management.area.report'
@@ -10,117 +13,227 @@ class ManagementAreaReport(models.Model):
@api.model
def get_management_area_report(self, params):
- """管理面积报表数据源。"""
- domain = []
+ """管理面积报表数据源。
- keyword = params.get("keyword")
- # 单日期模式,前端传 date_start
+ 核心逻辑:
+ 1. 数据源为 manage.information.line(管理信息明细行)
+ 2. 日期过滤:生效日期 <= 查询日期 <= 失效日期(失效日期为空表示至今有效)
+ 3. 管理面积直接取 manage.information.line.management_area
+ 4. 产权证号、产权地址从 title.information.line 取(也按日期范围匹配)
+ 5. 片区管理员取 precinct_id.display_name(格式:片区名称+管理员姓名)
+ 6. 合同编号、承租方从 property.lease.contract.line 反查合同获取
+ 7. 支持按物业名称(property_id)、承租方(lessee_id)、合同编号(contract_id)搜索
+ """
+
+ # ========== 一、参数解析 ==========
date_start = params.get("date_start")
date_end = params.get("date_end")
- # 兼容单日期和日期范围,优先取 date_start
+ # 单日期模式,优先取 date_start
query_date_str = date_start or date_end
+ keyword = params.get("keyword")
condition = params.get('condition')
partner_id = params.get('partner_id')
company_id = params.get('company_id')
+ # 前端侧边栏下拉选择器传入的 ID
+ property_id = params.get('property_id')
+ lessee_id = params.get('lessee_id')
+ contract_id = params.get('contract_id')
- # 构建管理信息明细行的查询域
- domain = []
- query_date_obj = None
-
- # 处理日期过滤:失效日期晚于查询日期,或没有失效日期
+ # ========== 二、查询日期解析 ==========
+ query_date = None
if query_date_str:
try:
- # 将带时间后缀的格式截断,如 "2024-11-01 00:00:00" -> "2024-11-01"
- query_date_obj = datetime.datetime.fromisoformat(query_date_str.split(' ')[0]).date()
- domain += [
- '|', ('end_date', '>=', query_date_obj), ('end_date', '=', False)
- ]
+ # 截断时间后缀,如 "2024-11-01 00:00:00" -> "2024-11-01"
+ query_date = datetime.datetime.fromisoformat(query_date_str.split(' ')[0]).date()
except Exception:
+ _logger.warning("[MANAGEMENT_AREA] 日期解析失败: %s", query_date_str)
pass
+ # 没有查询日期时不做时间过滤(返回所有记录)
+ domain = [('yuthon_property_id', '!=', False)]
+
+ # 日期过滤:生效日期 <= 查询日期,且(失效日期 >= 查询日期 或 失效日期为空)
+ if query_date:
+ domain += [
+ ('start_time', '<=', query_date),
+ '|', ('end_date', '>=', query_date), ('end_date', '=', False),
+ ]
+
+ # 其他筛选条件
if condition:
domain.append(('tenure_type', '=', condition))
if partner_id:
domain.append(('precinct_id', '=', partner_id))
if company_id:
domain.append(('company_id', '=', company_id))
+ # 物业名称下拉选择器:直接过滤 yuthon_property_id
+ if property_id:
+ domain.append(('yuthon_property_id', '=', int(property_id)))
- # 查询管理信息明细行(过滤掉没有关联物业的无效行)
- domain.append(('yuthon_property_id', '!=', False))
- manage_lines = self.env['manage.information.line'].search(domain)
+ # ========== 三、查询管理信息明细行 ==========
+ manage_lines = self.env['manage.information.line'].sudo().search(domain)
+ # ========== 四、批量预取合同信息 ==========
+ # 避免每条记录单独查询,一次性查出所有物业对应的合同
+ # 关联路径:manage.information.line → yuthon_property_id → yuthon.property
+ # ← property.lease.contract.line(address_id) → lease_id → property.lease.contract
+ property_ids = list({line.yuthon_property_id.id for line in manage_lines if line.yuthon_property_id})
+ # 合同映射:property_id → 合同信息(contract_code, lessee_name, contract_id)
+ contract_map = {}
+ if property_ids:
+ # 搜索所有关联的合同明细行
+ contract_lines = self.env['property.lease.contract.line'].sudo().search([
+ ('address_id', 'in', property_ids),
+ ])
+ for cl in contract_lines:
+ contract = cl.lease_id
+ if not contract:
+ continue
+ pid = cl.address_id.id
+ # 同一物业可能有多份合同,用列表存储
+ if pid not in contract_map:
+ contract_map[pid] = []
+ contract_map[pid].append({
+ 'contract_id': contract.id,
+ 'contract_code': contract.contract_code or "",
+ 'lessee_id': contract.lessee_id.id if contract.lessee_id else False,
+ 'lessee_name': contract.lessee_id.name if contract.lessee_id else "",
+ 'actual_start_time': contract.actual_start_time,
+ 'actual_end_time': contract.actual_end_time,
+ 'is_contract': contract.is_contract,
+ })
+
+ # ========== 五、承租方/合同编号过滤 ==========
+ # 前端传入 lessee_id 或 contract_id 时,需要根据合同信息过滤
+ # 找出符合条件的物业 ID 集合,用于过滤 manage_lines
+ if lessee_id or contract_id:
+ allowed_property_ids = set()
+ for pid, contracts in contract_map.items():
+ for c in contracts:
+ # 承租方匹配
+ if lessee_id and c['lessee_id'] != int(lessee_id):
+ continue
+ # 合同编号匹配
+ if contract_id and c['contract_id'] != int(contract_id):
+ continue
+ allowed_property_ids.add(pid)
+ break
+ # 过滤 manage_lines,只保留符合条件的物业
+ manage_lines = manage_lines.filtered(
+ lambda l: l.yuthon_property_id.id in allowed_property_ids
+ )
+
+ # ========== 六、构建报表行 ==========
rows = []
total_management_area = 0.0
- total_rent_area = 0.0
- total_total_area = 0.0
for idx, line in enumerate(manage_lines, start=1):
- # 获取物业信息
- property_id = line.yuthon_property_id
- if not property_id:
+ property_rec = line.yuthon_property_id
+ if not property_rec:
continue
- # 搜索关键词过滤
+ # --- 6.1 取合同信息(查询日期范围内生效的合同) ---
+ contract_code = ""
+ contract_record_id = False
+ lessee_name = ""
+ lessee_record_id = False
+ contracts = contract_map.get(property_rec.id, [])
+ if contracts:
+ # 优先取查询日期范围内生效的主合同
+ active_contracts = contracts
+ if query_date:
+ # 过滤:合同生效日期 <= 查询日期 且 合同失效日期 >= 查询日期
+ active_contracts = [
+ c for c in contracts
+ if c['actual_start_time'] and c['actual_start_time'] <= query_date
+ and (not c['actual_end_time'] or c['actual_end_time'] >= query_date)
+ ]
+ # 优先取主合同(is_contract=True)
+ main_contracts = [c for c in active_contracts if c['is_contract']]
+ target = main_contracts[0] if main_contracts else (active_contracts[0] if active_contracts else None)
+ if target:
+ contract_code = target['contract_code']
+ contract_record_id = target['contract_id']
+ lessee_name = target['lessee_name']
+ lessee_record_id = target['lessee_id']
+
+ # --- 6.2 关键字过滤(物业名称、承租方、合同编号) ---
if keyword:
- if not (
- (property_id.name and keyword in property_id.name) or
- (property_id.title_deed and keyword in property_id.title_deed) or
- (property_id.code and keyword in property_id.code)
- ):
+ match = False
+ kw = keyword.lower()
+ # 物业名称
+ if property_rec.name and kw in property_rec.name.lower():
+ match = True
+ # 承租方
+ if lessee_name and kw in lessee_name.lower():
+ match = True
+ # 合同编号
+ if contract_code and kw in contract_code.lower():
+ match = True
+ if not match:
continue
- # 获取管理类型
- management_type = line.purpose_id.name if line.purpose_id else ""
+ # --- 6.3 取产权信息(产权证号、产权地址) ---
+ # 产权信息也有生效/失效日期,按查询日期范围匹配
+ title_certificate = ""
+ property_address = ""
+ if property_rec.title_information_ids:
+ valid_titles = property_rec.title_information_ids
+ if query_date:
+ valid_titles = valid_titles.filtered(
+ lambda t: t.start_time and t.start_time <= query_date and
+ (not t.end_date or t.end_date >= query_date)
+ )
+ if valid_titles:
+ # 按生效日期降序取最新的
+ latest_title = valid_titles.sorted('start_time', reverse=True)[0]
+ title_certificate = latest_title.title_certificate or ""
+ if latest_title.property_message_id:
+ property_address = latest_title.property_message_id.name or ""
- # 获取合同编号和总面积
- contract_code = ""
- total_area = 0.0
- contract_domain = [('yuthon_property_id', '=', property_id.id)]
+ # --- 6.4 片区管理员 ---
+ # precinct_id.display_name 格式为 "片区名称管理员姓名",如 "二D梁嘉辉"
+ admin_display = ""
+ if line.precinct_id:
+ admin_display = line.precinct_id.display_name or ""
- if query_date_str:
- contract_domain += [
- '|', ('end_date', '>=', query_date_obj), ('end_date', '=', False)
- ]
-
- contract_lines = self.env['property.contract.line'].search(contract_domain, limit=1)
- if contract_lines:
- lease_contract = contract_lines.lease_contract_id
- if lease_contract:
- contract_code = lease_contract.contract_code or ""
- total_area = lease_contract.total_area or 0.0
-
- # 累计面积
- total_management_area += line.management_area or 0.0
- total_rent_area += line.rent_area or 0.0
- total_total_area += total_area
+ # --- 6.5 累计统计 ---
+ management_area = line.management_area or 0.0
+ total_management_area += management_area
+ # --- 6.6 构建行数据(物业名称、合同编号、承租方支持穿透) ---
rows.append({
"序号": idx,
- "物业id": property_id.id,
- "产权证号": property_id.title_deed or "",
- "物业名称": property_id.name or "",
- "失效日期": line.end_date.strftime("%Y-%m-%d") if line.end_date else "",
- "管理类型": management_type,
- "管理面积": round(line.management_area or 0.0, 4),
- "可租面积": round(line.rent_area or 0.0, 4),
- "合同编号": contract_code,
- "总面积": round(total_area, 4),
+ "物业id": property_rec.id,
+ "物业名称": {
+ 'model': 'yuthon.property',
+ 'id': property_rec.id,
+ 'display_name': property_rec.name or "",
+ } if property_rec.name else "",
+ "合同编号": {
+ 'model': 'property.lease.contract',
+ 'id': contract_record_id,
+ 'display_name': contract_code,
+ } if contract_record_id else contract_code or "",
+ "承租方": {
+ 'model': 'property.tenant.information',
+ 'id': lessee_record_id,
+ 'display_name': lessee_name,
+ } if lessee_record_id else lessee_name or "",
+ "产权证号": title_certificate,
+ "产权地址": property_address,
+ "管理面积": round(management_area, 4),
+ "片区管理员": admin_display,
})
- return {
- "rows": rows,
- "summary": {
- "序号": False,
- "物业id": "合计",
- "产权证号": False,
- "物业名称": False,
- "失效日期": False,
- "管理类型": False,
- "管理面积": round(total_management_area, 4),
- "可租面积": round(total_rent_area, 4),
- "合同编号": False,
- "总面积": round(total_total_area, 4),
- }
- }
+ # ========== 七、构建统计汇总数据 ==========
+ stats = [
+ {'name': '记录总数', 'amount': len(rows)},
+ {'name': '管理面积合计', 'amount': round(total_management_area, 4)},
+ ]
+ _logger.info("[MANAGEMENT_AREA] query_date=%s, lines=%d, rows=%d, total_area=%.4f",
+ query_date, len(manage_lines), len(rows), total_management_area)
+
+ return {"rows": rows, "stats": stats}
diff --git a/yuthon_report/models/penalty_arrears_report.py b/yuthon_report/models/penalty_arrears_report.py
index 5c907f99c..8d27c8d3f 100644
--- a/yuthon_report/models/penalty_arrears_report.py
+++ b/yuthon_report/models/penalty_arrears_report.py
@@ -1,231 +1,423 @@
-import datetime
+import datetime # 日期时间处理模块,用于日期解析、计算、格式化
+import logging # 日志模块,用于调试输出
-from odoo import api, models
+from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类
+
+_logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
class PenaltyArrearsReport(models.Model):
- _inherit = 'property.accounts.receivable'
+ _inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法
@api.model
def get_penalty_arrears_report(self, params):
"""
- 违约金累计欠收报表:
- - 筛选违约金记录(is_penalty=True 或 management_type_id 含"违约")
- - 只取未收款:submitted > 0
- - 每条记录单独一行(不按合同分组)
- - 金额字段与违约金明细报表一致
+ 违约金累计欠收报表(动态计算版)
+
+ 核心逻辑:
+ 1. Track1 — 仍有欠款的租金记录 → 分段动态计算违约金到查询日期
+ 2. Track2 — 已结清租金的未收违约金记录 → 取违约金记录的固定金额
+ 3. 关联方式:反查 — 同一合同+承租方,违约金.due_date == 租金.payout_date
+ 4. 计算规则:
+ a) 应收月的次月1号开始计算违约金
+ b) 违约金按每日千分之0.1%(0.001)计算
+ c) 租金部分支付后,剩余部分从前一次缴费日期次日开始重新计算
+ 5. 无收款日期的租金 = 一直没交钱 → 动态违约金(实时计算到查询月)
"""
- penalty_types = self.env['yuthon.management.type'].search([('name', 'ilike', '违约')])
- domain = [
- ('submitted', '>', 0),
- ('is_invalid', '!=', True),
+ # ========== 一、参数解析 ==========
+ date_end = params.get("date_end") # 查询截止日期(如 "2026-01-01 23:59:59")
+ query_date = None # 查询日期 date 对象,初始化为空
+ if date_end: # 如果有截止日期参数
+ query_date = datetime.datetime.fromisoformat(date_end).date() # 将字符串转为 date 对象
+
+ # ========== 二、构建公共筛选条件 ==========
+ # 关键字搜索:合同编号、物业名称、承租方名称三选一模糊匹配
+ keyword = params.get("keyword") # 获取关键字参数
+ # 其他筛选条件参数
+ condition = params.get('condition') # 租赁类型(如"商铺"、"写字楼")
+ partner_id = params.get('partner_id') # 片区 ID
+ company_id = params.get('company_id') # 公司 ID
+ property_id = params.get('property_id') # 物业 ID
+ lessee_id = params.get('lessee_id') # 承租方 ID
+ contract_id = params.get('contract_id') # 合同 ID
+
+ # 公共 domain 片段生成函数(租金和违约金共用)
+ def _append_common_filters(domain):
+ """将公共筛选条件追加到 domain 中"""
+ if keyword: # 如果有关键字
+ domain += ['|', '|', # Odoo 前缀表达式:两个 | 实现三条件 OR
+ ('contract_id.contract_code', 'ilike', keyword), # 合同编号模糊匹配
+ ('property_name', 'ilike', keyword), # 物业名称模糊匹配
+ ('lessee_id.name', 'ilike', keyword), # 承租方名称模糊匹配
+ ]
+ if condition: # 如果有租赁类型
+ domain.append(('tenure_type', '=', condition)) # 按租赁类型过滤
+ if partner_id: # 如果有片区
+ domain.append(('precinct_id', '=', partner_id)) # 按片区过滤
+ if company_id: # 如果有公司
+ domain.append(('company_id', '=', company_id)) # 按公司过滤
+ if property_id: # 如果有物业
+ domain.append(('property_ids', 'in', [property_id])) # 按物业过滤(Many2many 用 in)
+ if lessee_id: # 如果有承租方
+ domain.append(('lessee_id', '=', lessee_id)) # 按承租方过滤
+ if contract_id: # 如果有合同
+ domain.append(('contract_id', '=', contract_id)) # 按合同过滤
+ return domain # 返回追加条件后的 domain
+
+ # ========== 三、搜索仍有欠款的租金记录(Track1 数据源)==========
+ rent_domain = [
+ ('management_type_id.name', '=', '租金'), # 费用类型为租金
+ ('deposit_state', 'in', ['unpaid', 'no_paid']), # 状态为未收或收部分(仍有欠款)
+ ('is_liquidated_damages', '=', True), # 允许生成违约金标记
+ ('is_invalid', '!=', True), # 排除已作废记录
]
+ rent_domain = _append_common_filters(rent_domain) # 追加公共筛选条件
- date_start = params.get("date_start")
- date_end = params.get("date_end")
- if date_start:
- domain.append(('due_date', '>=', datetime.datetime.fromisoformat(date_start).date()))
- if date_end:
- domain.append(('due_date', '<=', datetime.datetime.fromisoformat(date_end).date()))
+ # 用 sudo() 提权搜索租金记录,按合同+应收日期升序排列
+ rent_lines = self.sudo().search(rent_domain, order="contract_id asc, due_date asc")
- # 违约金过滤
- if penalty_types:
- domain += ['|', ('management_type_id', 'in', penalty_types.ids), ('is_penalty', '=', True)]
- else:
- domain.append(('is_penalty', '=', True))
+ # 内存时间过滤:只保留 due_date <= 查询日期的租金记录
+ if query_date: # 如果有查询日期
+ rent_lines = rent_lines.filtered(lambda l: l.due_date and l.due_date <= query_date) # 过滤:应收日期 <= 查询日期
- keyword = params.get("keyword")
- if keyword:
- domain += [
- '|', '|',
- ('contract_id.contract_code', 'ilike', keyword),
- ('property_name', 'ilike', keyword),
- ('lessee_id.name', 'ilike', keyword),
- ]
+ # ========== 四、预查违约金记录,建立关联索引 ==========
+ # 一次性查出所有非作废的违约金记录,避免在循环中逐条查询
+ all_penalties = self.sudo().search([
+ ('management_type_id.name', '=', '违约金'), # 类型为违约金
+ ('is_invalid', '!=', True), # 排除已作废
+ ])
+ # 索引1:(contract_id, lessee_id, due_date) -> penalty_rec(用于无 source_receivable_id 时备用反查)
+ penalty_index = {} # 初始化索引字典
+ # 索引2:source_receivable_id -> penalty_rec(用于 Track1 快速关联)
+ penalty_by_source = {} # 按原始应收ID索引的违约金字典
+ for p in all_penalties: # 遍历所有违约金记录
+ key = (p.contract_id.id, p.lessee_id.id, p.due_date) # 构建索引键:合同ID+承租方ID+应收开始日期
+ if key not in penalty_index: # 如果键不存在(取第一条匹配,避免重复)
+ penalty_index[key] = p # 存入索引
+ # 如果违约金有 source_receivable_id(指向原始租金记录),建立反向索引
+ if p.source_receivable_id: # 如果违约金记录了原始应收(租金)ID
+ penalty_by_source[p.source_receivable_id.id] = p # 按租金ID索引
- condition = params.get('condition')
- partner_id = params.get('partner_id')
- company_id = params.get('company_id')
+ # ========== 五、初始化报表数据 ==========
+ rows = [] # 报表行数据列表
+ total_penalty = 0.0 # 违约金总额累计
+ total_collected = 0.0 # 违约金已收累计
+ total_uncollected = 0.0 # 违约金未收累计
+ processed_penalty_ids = set() # 已处理的违约金记录 ID 集合(避免 Track2 重复计算)
- if condition:
- domain.append(('tenure_type', '=', condition))
- if partner_id:
- domain.append(('precinct_id', '=', partner_id))
- if company_id:
- domain.append(('company_id', '=', company_id))
+ # ========== 六、Track1:仍有欠款的租金记录 → 分段动态计算违约金 ==========
+ for rent_rec in rent_lines: # 遍历每条仍有欠款的租金记录
+ due_date = rent_rec.due_date # 租金应收日期
+ if not due_date: # 如果没有应收日期
+ continue # 跳过该记录
- receivables = self.search(domain, order="contract_id asc, due_date asc")
+ # --- 6.1 计算违约金起始日期:应收月的次月1号 ---
+ if due_date.month == 12: # 12月特殊处理:次年1月1日
+ penalty_start = datetime.date(due_date.year + 1, 1, 1) # 次年1月1日
+ else: # 其他月份:当月+1月的1日
+ penalty_start = datetime.date(due_date.year, due_date.month + 1, 1) # 次月1日
- rows = []
- total_penalty = total_collected = total_uncollected = 0.0
+ # 如果违约金起始日期 > 查询日期,说明还没开始产生违约金
+ if query_date and penalty_start > query_date: # 尚未产生违约金
+ continue # 跳过该记录
- for idx, rec in enumerate(receivables, start=1):
- # 计算欠租金额(直接使用合同的rent字段,即月租金)
- rent_owed = 0.0
- if rec.contract_id:
- rent_owed = rec.contract_id.rent or 0.0
- # 如果合同中没有,则使用原始金额
- rent_owed = rent_owed or rec.rent_owed or 0.0
-
- # 计算每天违约金(欠租金额 × 违约金比例)
- per_day = 0.0
- if rent_owed > 0 and rec.contract_id and rec.contract_id.percentage_liquidated_damages:
- # 获取违约金比例
- penalty_ratio = rec.contract_id.percentage_liquidated_damages.name or "0"
- try:
- # 尝试将比例字符串转换为浮点数
- penalty_ratio = float(penalty_ratio)
- # 计算每天违约金
- per_day = rent_owed * penalty_ratio
- except ValueError:
- # 如果转换失败,使用默认值
- per_day = 0.0
- # 如果无法计算,则使用现有字段
- per_day = per_day or rec.difference_amount or rec.per_day or 0.0
-
- # 计算违约金金额(每天违约金 × 违约天数)
- penalty_amount = 0.0
- # 尝试从charging_range字段获取时间区间
- billing_period = rec.charging_range or ""
- days_diff = 0
-
- if billing_period:
- # 尝试从charging_range中提取天数
- try:
- # 假设charging_range格式为"2026-02-01 ~ 2026-02-28"
- date_parts = billing_period.split(" ~ ")
- if len(date_parts) == 2:
- start_date_str = date_parts[0].strip()
- end_date_str = date_parts[1].strip()
- start_date = datetime.datetime.strptime(start_date_str, "%Y-%m-%d").date()
- end_date = datetime.datetime.strptime(end_date_str, "%Y-%m-%d").date()
- days_diff = (end_date - start_date).days + 1
- # 构建标准格式的时间区间字符串
- billing_period = f"{days_diff} {start_date_str} ~ {end_date_str}"
- except Exception:
- # 如果解析失败,使用默认计算
- if rec.due_date:
- # 固定开始日期为当月第一天
- start_date = rec.due_date.replace(day=1)
- # 确定结束日期
- if rec.payout_date:
- # 已缴费,结束日期为缴费日期
- end_date = rec.payout_date
- else:
- # 未缴费,结束日期为查询月的最后一天
- if date_end:
- end_date_obj = datetime.datetime.fromisoformat(date_end).date()
- # 获取查询月的最后一天
- if end_date_obj.month == 12:
- last_day = datetime.date(end_date_obj.year, 12, 31)
- else:
- last_day = datetime.date(end_date_obj.year, end_date_obj.month + 1, 1) - datetime.timedelta(days=1)
- end_date = last_day
- else:
- # 无查询日期,使用当前月最后一天
- today = datetime.date.today()
- if today.month == 12:
- last_day = datetime.date(today.year, 12, 31)
- else:
- last_day = datetime.date(today.year, today.month + 1, 1) - datetime.timedelta(days=1)
- end_date = last_day
-
- # 计算违约天数
- days_diff = (end_date - start_date).days + 1 # 包含开始和结束日期
-
- # 构建时间区间字符串
- billing_period = f"{days_diff} {start_date.strftime('%Y-%m-%d')} ~ {end_date.strftime('%Y-%m-%d')}"
- else:
- # 如果charging_range为空,使用默认计算
- if rec.due_date:
- # 固定开始日期为当月第一天
- start_date = rec.due_date.replace(day=1)
- # 确定结束日期
- if rec.payout_date:
- # 已缴费,结束日期为缴费日期
- end_date = rec.payout_date
- else:
- # 未缴费,结束日期为查询月的最后一天
- if date_end:
- end_date_obj = datetime.datetime.fromisoformat(date_end).date()
- # 获取查询月的最后一天
- if end_date_obj.month == 12:
- last_day = datetime.date(end_date_obj.year, 12, 31)
- else:
- last_day = datetime.date(end_date_obj.year, end_date_obj.month + 1, 1) - datetime.timedelta(days=1)
- end_date = last_day
- else:
- # 无查询日期,使用当前月最后一天
- today = datetime.date.today()
- if today.month == 12:
- last_day = datetime.date(today.year, 12, 31)
- else:
- last_day = datetime.date(today.year, today.month + 1, 1) - datetime.timedelta(days=1)
- end_date = last_day
-
- # 计算违约天数
- days_diff = (end_date - start_date).days + 1 # 包含开始和结束日期
-
- # 构建时间区间字符串
- billing_period = f"{days_diff} {start_date.strftime('%Y-%m-%d')} ~ {end_date.strftime('%Y-%m-%d')}"
-
- # 计算违约金金额
- penalty_amount = days_diff * per_day
- # 如果计算结果为0,使用现有字段
- penalty_amount = penalty_amount or rec.penalty or rec.original_amount or 0.0
+ # --- 6.2 获取违约金计算比例 ---
+ # 优先使用合同配置的比例(percentage_liquidated_damages.name),否则使用 0.001
+ penalty_rate = 0.001 # 默认每日千分之一(0.1%)
+ contract = rent_rec.contract_id # 获取租金关联的合同
+ if contract and contract.percentage_liquidated_damages: # 如果合同有违约金比例配置
+ try: # 尝试读取合同配置的比例
+ rate_val = float(contract.percentage_liquidated_damages.name) # 将配置值转为浮点数
+ if rate_val > 0: # 比例值有效(大于0)
+ penalty_rate = rate_val # 使用合同配置的比例
+ except (ValueError, TypeError): # 转换失败时保持默认值
+ pass # 忽略异常,继续使用默认 0.001
- amount = rec.original_amount or 0.0
-
- collected = rec.delivered1 or 0.0
- uncollected = rec.submitted or 0.0
+ # --- 6.3 获取租金的支付历史(非违约金的 modify lines) ---
+ modify_lines = self.env['receivable.modify.line'].sudo().search([
+ ('receivable_id', '=', rent_rec.id), # 属于当前租金记录
+ ('reconciliation_date', '!=', False), # 有对账日期(已确认付款)
+ ('is_penalty', '=', False), # 非违约金支付(只取租金支付记录)
+ ], order='reconciliation_date asc') # 按对账日期升序排列
- total_penalty += penalty_amount
- total_collected += collected
- total_uncollected += uncollected
+ # --- 6.4 分段计算违约金 ---
+ # 核心算法:按支付日期分段,每段独立计算违约金
+ # 规则3:租金部分支付后,剩余部分从前一次缴费日期次日开始重新计算
+ rent_amount = (rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0) # 租金金额 = 原金额 - 折扣
+ current_principal = rent_amount # 当前剩余本金(初始为全部租金金额)
+ current_start = penalty_start # 当前计算起始日期(初始为违约金起始日期)
+ total_calc_penalty = 0.0 # 累计违约金金额
+ calc_range_parts = [] # 计算区间片段列表(用于展示计算明细)
+ for line in modify_lines: # 遍历每条支付记录(按日期升序)
+ pay_date = line.reconciliation_date # 本次支付日期
+ pay_amount = line.amount or 0.0 # 本次支付金额
+
+ # 支付日期在当前计算起点之前:直接扣减本金,不产生违约金
+ if pay_date < current_start: # 支付早于违约金起始
+ current_principal = max(0, current_principal - pay_amount) # 扣减本金
+ continue # 跳到下一条支付记录
+
+ # 支付日期 == 当前计算起点:当天既算违约金最后一天,也是支付日
+ # 支付日期 > 当前计算起点:正常分段计算
+ # 两种情况统一处理:计算从 current_start 到 pay_date 的违约金(包含两端)
+ days = (pay_date - current_start).days + 1 # 逾期天数(含首尾两天)
+ if days > 0 and current_principal > 0: # 有逾期天数且本金大于0
+ segment_penalty = current_principal * days * penalty_rate # 本段违约金 = 本金 × 天数 × 比例
+ total_calc_penalty += segment_penalty # 累加到总违约金
+ calc_range_parts.append( # 记录本段计算明细
+ f"{current_start.strftime('%Y-%m-%d')}至{pay_date.strftime('%Y-%m-%d')} "
+ f"({days}天×{current_principal:.2f}×{penalty_rate})"
+ )
+
+ # 规则3:支付后,剩余本金从次日重新计算
+ current_principal = max(0, current_principal - pay_amount) # 扣减已支付金额,得到剩余本金
+ current_start = pay_date + datetime.timedelta(days=1) # 新起点 = 本次支付日的次日
+
+ # --- 6.5 最后一段:从最后一次支付(或起始日)到查询日期 ---
+ # 对于未结清的租金,违约金持续计算到查询日期
+ if current_principal > 0 and query_date and current_start <= query_date: # 还有剩余本金且在查询范围内
+ days = (query_date - current_start).days + 1 # 逾期天数(含首尾两天)
+ if days > 0: # 有逾期天数
+ segment_penalty = current_principal * days * penalty_rate # 最后一段违约金
+ total_calc_penalty += segment_penalty # 累加
+ calc_range_parts.append( # 记录最后一段计算明细
+ f"{current_start.strftime('%Y-%m-%d')}至{query_date.strftime('%Y-%m-%d')} "
+ f"({days}天×{current_principal:.2f}×{penalty_rate})"
+ )
+
+ total_calc_penalty = round(total_calc_penalty, 2) # 四舍五入保留2位小数
+
+ if total_calc_penalty <= 0: # 如果计算出的违约金为0或负数
+ continue # 跳过该记录
+
+ # --- 6.6 关联违约金记录 ---
+ # 优先通过 source_receivable_id 直接关联(违约金记录中存储了指向原始租金的ID)
+ penalty_rec = None # 初始化为空
+ if rent_rec.id in penalty_by_source: # 如果该租金在违约金索引中有记录
+ penalty_rec = penalty_by_source[rent_rec.id] # 直接获取关联的违约金记录
+
+ if penalty_rec: # 如果找到了违约金记录
+ processed_penalty_ids.add(penalty_rec.id) # 标记为已处理,避免 Track2 重复计算
+
+ # --- 6.7 获取已收违约金 ---
+ penalty_collected = 0.0 # 初始化已收违约金金额
+ if penalty_rec: # 如果有违约金记录
+ # 查询违约金记录自身的收款明细
+ p_modify = self.env['receivable.modify.line'].sudo().search([
+ ('receivable_id', '=', penalty_rec.id), # 属于该违约金记录
+ ('reconciliation_date', '!=', False), # 有对账日期(已确认付款)
+ ])
+ penalty_collected = round(sum(l.amount or 0.0 for l in p_modify), 2) # 已收金额 = 所有付款行金额之和
+
+ penalty_uncollected = round(max(0, total_calc_penalty - penalty_collected), 2) # 违约金未收 = 计算金额 - 已收(不低于0)
+
+ # --- 6.8 租金信息 ---
+ rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2) # 租金已收金额
+ rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2) # 租金未收金额
+ rent_status = "" # 租金收款状态文字
+ if rent_rec.submitted == 0: # submitted 为 0 表示没有未收(即已全额收款)
+ rent_status = "已收"
+ elif rent_rec.delivered1 and rent_rec.delivered1 > 0: # delivered1 > 0 表示有部分收款
+ rent_status = "收部分"
+ else: # 既非已收也非收部分,则为未收
+ rent_status = "未收"
+
+ # --- 6.9 违约金状态 ---
+ penalty_status = "" # 初始化违约金状态文字
+ if penalty_rec: # 如果有违约金记录
+ # 从 deposit_state 字段的 selection 选项中获取中文显示名
+ penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
+ penalty_rec.deposit_state, penalty_rec.deposit_state)
+ else: # 没有违约金记录(动态违约金)
+ penalty_status = "动态" # 标记为动态违约金
+
+ # --- 6.10 违约金应收开始日期 ---
+ if penalty_rec and penalty_rec.due_date: # 如果有违约金记录且有应收开始日期
+ penalty_due_date_str = penalty_rec.due_date.strftime("%Y-%m-%d") # 取违约金记录的应收开始
+ else: # 动态违约金,取计算出的起始日期
+ penalty_due_date_str = penalty_start.strftime("%Y-%m-%d") # 取次月1号作为应收开始
+
+ # --- 6.11 累计统计 ---
+ total_penalty += total_calc_penalty # 累加违约金总额
+ total_collected += penalty_collected # 累加违约金已收
+ total_uncollected += penalty_uncollected # 累加违约金未收
+
+ # --- 6.12 构建物业名称显示 ---
+ property_display = "" # 初始化物业名称
+ if contract and contract.lease_ids and contract.lease_ids[0].address_id: # 如果合同有租赁明细且有物业地址
+ property_display = { # 带跳转链接的物业名称
+ 'model': 'yuthon.property', # 跳转目标模型
+ 'id': contract.lease_ids[0].address_id.id, # 物业记录 ID
+ 'display_name': rent_rec.property_name or "", # 显示名称
+ }
+ else: # 没有物业地址时
+ property_display = rent_rec.property_name or "" # 纯文本显示
+
+ # --- 6.13 构建收费区间显示 ---
+ # 动态违约金:显示分段计算明细;有违约金记录:优先使用记录的收费区间
+ if calc_range_parts: # 如果有动态计算明细
+ charging_range_display = "\n".join(calc_range_parts) # 用换行拼接各段明细
+ elif penalty_rec and penalty_rec.charging_range: # 如果有违约金记录的收费区间
+ charging_range_display = penalty_rec.charging_range # 使用记录的收费区间
+ else: # 都没有
+ charging_range_display = "" # 显示空
+
+ # --- 6.14 构建行数据 ---
rows.append({
- "序号": idx,
- "合同编号": {
- 'model': 'property.lease.contract',
- 'id': rec.contract_id.id,
- 'display_name': rec.contract_id.contract_code,
- } if rec.contract_id else "",
- "物业名称": {
- 'model': 'yuthon.property',
- 'id': rec.contract_id.lease_ids[0].address_id.id,
- 'display_name': rec.property_name,
- } if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "",
- "承租方名称": {
- 'model': 'property.tenant.information',
- 'id': rec.lessee_id.id,
- 'display_name': rec.lessee_id.name,
- } if rec.lessee_id else "",
- "金额": round(amount, 2),
- "应收金额": round((rec.original_amount or 0.0) - (rec.discount_amount or 0.0), 2),
- "违约计算时间(天)": billing_period,
- "欠租金额": round(rent_owed, 2),
- "每天违约金(元/天)": round(per_day, 2),
- "违约金金额": round(penalty_amount, 2),
- "总已收金额": round(collected, 2),
- "总未收": round(uncollected, 2),
- "当月收款时间": rec.payout_date.strftime("%Y-%m-%d") if rec.payout_date else "",
- "当月收款金额": round(collected, 2),
- "所属公司": rec.company_id.name if rec.company_id else "",
- "产权证号": rec.title_deed or "",
- "管理员": {
- 'model': 'res.users',
- 'id': rec.admin_id.id,
- 'display_name': rec.admin_id.name,
- } if rec.admin_id else "",
+ "租金ID": rent_rec.id, # 租金记录 ID
+ "租金应收日": due_date.strftime("%Y-%m-%d"), # 租金应收日期
+ "租金收款日期": rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "", # 租金收款日期(无则为空)
+ "租金金额": round(rent_amount, 2), # 租金金额(原金额-折扣)
+ "租金已收": rent_collected_amt, # 租金已收金额
+ "租金未收": rent_uncollected_amt, # 租金未收金额
+ "租金状态": rent_status, # 租金收款状态(已收/收部分/未收)
+ "违约金金额": total_calc_penalty, # 动态计算的违约金金额
+ "违约金已收": penalty_collected, # 违约金已收金额
+ "违约金未收": penalty_uncollected, # 违约金未收金额
+ "违约金状态": penalty_status, # 违约金状态(动态/未收/收部分)
+ "违约金应收开始": penalty_due_date_str, # 违约金应收开始日期
+ "收费区间": charging_range_display, # 收费区间(动态计算明细或记录值)
+ "备注": penalty_rec.remark if penalty_rec else "", # 备注
+ "合同编号": { # 带跳转链接的合同编号
+ 'model': 'property.lease.contract', # 跳转目标模型
+ 'id': contract.id, # 合同记录 ID
+ 'display_name': contract.contract_code, # 显示合同编号
+ } if contract else "", # 无合同时显示空
+ "物业名称": property_display, # 物业名称(带链接或纯文本)
+ "承租方名称": { # 带跳转链接的承租方名称
+ 'model': 'property.tenant.information', # 跳转目标模型
+ 'id': rent_rec.lessee_id.id, # 承租方记录 ID
+ 'display_name': rent_rec.lessee_id.name, # 显示承租方名称
+ } if rent_rec.lessee_id else "", # 无承租方时显示空
+ "所属公司": rent_rec.company_id.name if rent_rec.company_id else "", # 所属公司名称
})
+ # ========== 七、Track2:已结清租金的未收违约金记录 ==========
+ # 这些违约金对应的租金已经全额支付,但违约金本身还没收回来
+ # 对于已结清租金,违约金金额是固定的(取违约金记录的 original_amount)
+ penalty_domain_2 = [
+ ('management_type_id.name', '=', '违约金'), # 类型为违约金
+ ('deposit_state', 'in', ['unpaid', 'no_paid']), # 状态为未收或收部分
+ ('is_invalid', '!=', True), # 排除已作废
+ ]
+ penalty_domain_2 = _append_common_filters(penalty_domain_2) # 追加公共筛选条件
+
+ # 搜索未收违约金记录
+ uncollected_penalties = self.sudo().search(penalty_domain_2, order="contract_id asc, due_date asc")
+
+ # 内存时间过滤:只保留 due_date <= 查询日期的违约金记录
+ if query_date: # 如果有查询日期
+ uncollected_penalties = uncollected_penalties.filtered(
+ lambda l: l.due_date and l.due_date <= query_date) # 过滤:应收开始日期 <= 查询日期
+
+ for penalty_rec in uncollected_penalties: # 遍历每条未收违约金记录
+ if penalty_rec.id in processed_penalty_ids: # 如果已在 Track1 中处理过
+ continue # 跳过,避免重复计算
+
+ # --- 7.1 关联租金:通过 source_receivable_id 直接关联 ---
+ # 违约金记录中存储了 source_receivable_id 字段,指向产生该违约金的原始租金记录
+ rent_rec = penalty_rec.source_receivable_id # 直接取关联的租金记录
+ # 如果 rent_rec 仍有欠款,说明它应该已在 Track1(动态计算)中被处理
+ if rent_rec and rent_rec.deposit_state in ['unpaid', 'no_paid']: # 租金仍有欠款
+ continue # 跳过(Track1 已处理)
+
+ # --- 7.2 使用违约金记录的金额(固定违约金,因为租金已结清)---
+ penalty_amount = round(penalty_rec.original_amount or 0.0, 2) # 违约金金额取记录的 original_amount
+
+ # --- 7.3 获取已收违约金 ---
+ p_modify = self.env['receivable.modify.line'].sudo().search([
+ ('receivable_id', '=', penalty_rec.id), # 属于该违约金记录
+ ('reconciliation_date', '!=', False), # 有对账日期(已确认付款)
+ ])
+ penalty_collected = round(sum(l.amount or 0.0 for l in p_modify), 2) # 已收金额 = 所有付款行金额之和
+ penalty_uncollected = round(max(0, penalty_amount - penalty_collected), 2) # 未收 = 金额 - 已收(不低于0)
+
+ # --- 7.4 租金信息 ---
+ rent_amount_val = 0.0 # 租金金额
+ rent_collected_amt = 0.0 # 租金已收
+ rent_uncollected_amt = 0.0 # 租金未收
+ rent_status = "" # 租金状态
+ rent_due_date_str = "" # 租金应收日
+ rent_payout_date_str = "" # 租金收款日期
+
+ if rent_rec: # 如果找到了关联租金记录
+ rent_amount_val = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2) # 租金金额
+ rent_collected_amt = round(rent_rec.delivered1 or 0.0, 2) # 租金已收
+ rent_uncollected_amt = round(rent_rec.submitted or 0.0, 2) # 租金未收
+ rent_due_date_str = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else "" # 租金应收日
+ rent_payout_date_str = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "" # 收款日期
+ # 判断租金收款状态
+ if rent_rec.submitted == 0: # submitted 为 0 表示已全额收款
+ rent_status = "已收"
+ elif rent_rec.delivered1 and rent_rec.delivered1 > 0: # delivered1 > 0 表示有部分收款
+ rent_status = "收部分"
+ else: # 既非已收也非收部分
+ rent_status = "未收"
+
+ # --- 7.5 违约金状态 ---
+ penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
+ penalty_rec.deposit_state, penalty_rec.deposit_state) # 从 selection 取中文显示名
+
+ # --- 7.6 累计统计 ---
+ total_penalty += penalty_amount # 累加违约金总额
+ total_collected += penalty_collected # 累加违约金已收
+ total_uncollected += penalty_uncollected # 累加违约金未收
+
+ # --- 7.7 构建物业名称显示 ---
+ contract = penalty_rec.contract_id # 获取违约金记录关联的合同
+ property_display = "" # 初始化物业名称
+ if contract and contract.lease_ids and contract.lease_ids[0].address_id: # 如果合同有租赁明细且有物业地址
+ property_display = { # 带跳转链接的物业名称
+ 'model': 'yuthon.property', # 跳转目标模型
+ 'id': contract.lease_ids[0].address_id.id, # 物业记录 ID
+ 'display_name': penalty_rec.property_name or "", # 显示名称
+ }
+ else: # 没有物业地址时
+ property_display = penalty_rec.property_name or "" # 纯文本显示
+
+ # --- 7.8 构建行数据 ---
+ rows.append({
+ "租金ID": rent_rec.id if rent_rec else 0, # 租金记录 ID,无则为 0
+ "租金应收日": rent_due_date_str, # 租金应收日期
+ "租金收款日期": rent_payout_date_str, # 租金收款日期
+ "租金金额": rent_amount_val, # 租金金额
+ "租金已收": rent_collected_amt, # 租金已收
+ "租金未收": rent_uncollected_amt, # 租金未收
+ "租金状态": rent_status, # 租金状态
+ "违约金金额": penalty_amount, # 固定违约金金额(取违约金记录值)
+ "违约金已收": penalty_collected, # 违约金已收
+ "违约金未收": penalty_uncollected, # 违约金未收
+ "违约金状态": penalty_status, # 违约金状态
+ "违约金应收开始": penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "", # 违约金应收开始
+ "收费区间": penalty_rec.charging_range or "", # 收费区间
+ "备注": penalty_rec.remark or "", # 备注
+ "合同编号": { # 带跳转链接的合同编号
+ 'model': 'property.lease.contract', # 跳转目标模型
+ 'id': contract.id, # 合同记录 ID
+ 'display_name': contract.contract_code, # 显示合同编号
+ } if contract else "", # 无合同时显示空
+ "物业名称": property_display, # 物业名称(带链接或纯文本)
+ "承租方名称": { # 带跳转链接的承租方名称
+ 'model': 'property.tenant.information', # 跳转目标模型
+ 'id': penalty_rec.lessee_id.id, # 承租方记录 ID
+ 'display_name': penalty_rec.lessee_id.name, # 显示承租方名称
+ } if penalty_rec.lessee_id else "", # 无承租方时显示空
+ "所属公司": penalty_rec.company_id.name if penalty_rec.company_id else "", # 所属公司名称
+ })
+
+ # ========== 八、构建统计汇总数据 ==========
stats = [
- {'name': '记录总数', 'amount': len(rows)},
- {'name': '违约金总额', 'amount': round(total_penalty, 2)},
- {'name': '总已收', 'amount': round(total_collected, 2)},
- {'name': '总未收', 'amount': round(total_uncollected, 2)},
+ {'name': '记录总数', 'amount': len(rows)}, # 报表行数
+ {'name': '违约金总额', 'amount': round(total_penalty, 2)}, # 所有违约金金额之和
+ {'name': '违约金已收', 'amount': round(total_collected, 2)}, # 所有违约金已收之和
+ {'name': '违约金未收', 'amount': round(total_uncollected, 2)}, # 所有违约金未收之和
]
- return {"rows": rows, "stats": stats}
+ # 输出调试日志,记录参数、记录数和统计结果
+ _logger.info("[PENALTY_ARREARS] params=%s, track1_rents=%s, track2_penalties=%s, total_rows=%s, stats=%s",
+ params, len(rent_lines), len(uncollected_penalties) if 'uncollected_penalties' in dir() else 0,
+ len(rows), stats)
+ return {"rows": rows, "stats": stats} # 返回报表数据(行 + 统计)
diff --git a/yuthon_report/models/penalty_month_received_report.py b/yuthon_report/models/penalty_month_received_report.py
index eade5a031..af79d4b80 100644
--- a/yuthon_report/models/penalty_month_received_report.py
+++ b/yuthon_report/models/penalty_month_received_report.py
@@ -1,132 +1,236 @@
-import datetime
+import datetime # 日期时间处理模块,用于日期解析和计算
+import logging # 日志模块,用于调试输出
-from odoo import api, models
+from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类
+
+_logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
class PenaltyMonthReceivedReport(models.Model):
- _inherit = 'property.accounts.receivable'
+ _inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法
@api.model
def get_penalty_month_received_report(self, params):
"""
- 违约金本月实收报表:
- - 筛选违约金记录(is_penalty=True 或 management_type_id 含"违约")
- - 本月内有收款:payout_date 在本月第一天到最后一天之间
- - 每条记录单独一行,字段与违约金累计欠收一致
+ 违约金本月实收报表
+
+ 核心逻辑:
+ 1. 数据源:receivable.modify.line(is_penalty=True, reconciliation_date在查询月内)
+ 2. 每笔付款单独一行,不合并,展示每笔的收款日期和金额
+ 3. 关联租金:反查模式(同合同+承租方,违约金.due_date == 租金.payout_date)
+ 4. receivable可能是违约金记录或租金记录,两种都需要处理
"""
- today = datetime.date.today()
- this_month_first = today.replace(day=1)
- if today.month == 12:
- next_month_first = today.replace(year=today.year + 1, month=1, day=1)
- else:
- next_month_first = today.replace(month=today.month + 1, day=1)
- this_month_last = next_month_first - datetime.timedelta(days=1)
- penalty_types = self.env['yuthon.management.type'].search([('name', 'ilike', '违约')])
+ # ========== 一、参数解析 ==========
+ date_start = params.get("date_start") # 查询月开始日期(如 "2026-01-01 00:00:00")
+ date_end = params.get("date_end") # 查询月结束日期(如 "2026-01-31 23:59:59")
- date_start = params.get("date_start")
- date_end = params.get("date_end")
- start_dt = datetime.datetime.fromisoformat(date_start).date() if date_start else this_month_first
- end_dt = datetime.datetime.fromisoformat(date_end).date() if date_end else this_month_last
+ # 没有时间范围则无数据
+ if not date_start or not date_end:
+ return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
- domain = [
- ('payout_date', '>=', start_dt),
- ('payout_date', '<=', end_dt),
- ('is_invalid', '!=', True),
- ]
+ # 解析查询时间范围为 date 对象
+ ds = datetime.datetime.fromisoformat(date_start).date() # 查询月开始日期
+ de = datetime.datetime.fromisoformat(date_end).date() # 查询月结束日期
- # 违约金过滤
- if penalty_types:
- domain += ['|', ('management_type_id', 'in', penalty_types.ids), ('is_penalty', '=', True)]
- else:
- domain.append(('is_penalty', '=', True))
+ # 其他筛选条件参数
+ keyword = params.get("keyword") # 关键字搜索
+ condition = params.get('condition') # 租赁类型
+ partner_id = params.get('partner_id') # 片区 ID
+ company_id = params.get('company_id') # 公司 ID
+ property_id = params.get('property_id') # 物业 ID
+ lessee_id = params.get('lessee_id') # 承租方 ID
+ contract_id = params.get('contract_id') # 合同 ID
- keyword = params.get("keyword")
- if keyword:
- domain += [
- '|', '|',
- ('contract_id.contract_code', 'ilike', keyword),
- ('property_name', 'ilike', keyword),
- ('lessee_id.name', 'ilike', keyword),
- ]
+ # ========== 二、搜索查询月内违约金付款记录 ==========
+ # 直接从 receivable.modify.line 查找本月实收的违约金
+ modify_lines = self.env['receivable.modify.line'].sudo().search([
+ ('is_penalty', '=', True), # 标记为违约金付款
+ ('reconciliation_date', '!=', False), # 有对账日期(已确认付款)
+ ('reconciliation_date', '>=', ds), # 对账日期 >= 查询月开始
+ ('reconciliation_date', '<=', de), # 对账日期 <= 查询月结束
+ ])
- condition = params.get('condition')
- partner_id = params.get('partner_id')
- company_id = params.get('company_id')
+ # 没有付款记录则直接返回空
+ if not modify_lines:
+ return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
- if condition:
- domain.append(('tenure_type', '=', condition))
- if partner_id:
- domain.append(('precinct_id', '=', partner_id))
- if company_id:
- domain.append(('company_id', '=', company_id))
+ # ========== 三、构建关联索引 ==========
+ # 收集所有涉及的合同 ID,用于预查
+ contract_ids = set() # 合同 ID 集合
+ for line in modify_lines: # 遍历每条付款记录
+ rec = line.receivable_id # 获取关联的应收记录
+ if rec.contract_id: # 如果有关联合同
+ contract_ids.add(rec.contract_id.id) # 添加合同 ID 到集合
- receivables = self.search(domain, order="contract_id asc, due_date asc")
+ # 预查所有相关违约金记录(用于 source_receivable_id 为空时备用反查)
+ all_penalties = self.sudo().search([
+ ('management_type_id.name', '=', '违约金'), # 类型为违约金
+ ('is_invalid', '!=', True), # 排除已作废
+ ('contract_id', 'in', list(contract_ids)), # 在涉及的合同范围内
+ ])
- rows = []
- total_penalty = total_collected = total_uncollected = 0.0
+ # ========== 四、构建报表行(每笔付款单独一行) ==========
+ rows = [] # 报表行数据列表
+ total_collected = 0.0 # 本月实收违约金总计
- for idx, rec in enumerate(receivables, start=1):
- per_day = rec.per_day or 0.0
- penalty_amount = rec.penalty or 0.0
- penalty_days = round(penalty_amount / per_day, 2) if per_day else 0.0
+ for line in modify_lines: # 遍历每条违约金付款记录(不合并)
+ receivable = line.receivable_id # 获取关联的应收记录
+ pay_amount = round(line.amount or 0.0, 2) # 本笔收款金额
+ pay_date = line.reconciliation_date # 本笔收款日期
- amount = rec.original_amount or 0.0
- rent_owed = rec.rent_owed or 0.0
- collected = rec.delivered1 or 0.0
- uncollected = rec.submitted or 0.0
+ # 收款金额为0或负数则跳过
+ if pay_amount <= 0:
+ continue
- total_penalty += penalty_amount
- total_collected += collected
- total_uncollected += uncollected
+ # --- 4.1 内存过滤:应用关键字和条件筛选 ---
+ contract = receivable.contract_id # 获取关联合同
+ lessee = receivable.lessee_id # 获取关联承租方
- billing_period = ""
- if rec.due_date and rec.due_date_end:
- billing_period = f"{rec.due_date.strftime('%Y-%m-%d')} ~ {rec.due_date_end.strftime('%Y-%m-%d')}"
- elif rec.due_date:
- billing_period = rec.due_date.strftime("%Y-%m-%d")
+ # 关键字过滤
+ if keyword: # 如果有关键字条件
+ kw = keyword.lower() # 转小写用于匹配
+ match = False # 匹配标志
+ if contract and contract.contract_code and kw in contract.contract_code.lower(): # 合同编号匹配
+ match = True
+ if receivable.property_name and kw in receivable.property_name.lower(): # 物业名称匹配
+ match = True
+ if lessee and lessee.name and kw in lessee.name.lower(): # 承租方名称匹配
+ match = True
+ if not match: # 三项都不匹配
+ continue # 跳过该记录
+ # 租赁类型过滤
+ if condition and receivable.tenure_type != condition: # 租赁类型不匹配
+ continue
+
+ # 片区过滤
+ if partner_id and receivable.precinct_id.id != partner_id: # 片区不匹配
+ continue
+
+ # 公司过滤
+ if company_id and receivable.company_id.id != company_id: # 公司不匹配
+ continue
+
+ # 物业过滤
+ if property_id and property_id not in (receivable.property_ids.ids or []): # 物业不匹配
+ continue
+
+ # 承租方过滤
+ if lessee_id and (not lessee or lessee.id != lessee_id): # 承租方不匹配
+ continue
+
+ # 合同过滤
+ if contract_id and (not contract or contract.id != contract_id): # 合同不匹配
+ continue
+
+ # --- 4.2 判断 receivable 类型并关联租金 ---
+ # receivable 可能是违约金记录或租金记录
+ is_penalty_rec = receivable.management_type_id.name == '违约金' # 是否为违约金记录
+
+ rent_rec = None # 关联的租金记录
+ penalty_rec = None # 关联的违约金记录
+
+ if is_penalty_rec: # 如果 receivable 本身就是违约金记录
+ penalty_rec = receivable # 直接使用
+ # 通过 source_receivable_id 直接关联到原始租金记录
+ rent_rec = penalty_rec.source_receivable_id # 直接取违约金中存储的原始应收(租金)记录
+ else: # receivable 是租金记录
+ rent_rec = receivable # 直接使用
+
+ # --- 4.3 租金信息 ---
+ rent_amount = 0.0 # 租金金额
+ rent_collected = 0.0 # 租金已收
+ rent_uncollected = 0.0 # 租金未收
+ rent_status = "" # 租金状态
+ rent_due_date = "" # 租金应收日
+ rent_payout_date = "" # 租金收款日期
+
+ if rent_rec: # 如果找到了关联租金记录
+ rent_amount = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2) # 租金金额 = 原金额 - 折扣
+ rent_collected = round(rent_rec.delivered1 or 0.0, 2) # 租金已收
+ rent_uncollected = round(rent_rec.submitted or 0.0, 2) # 租金未收
+ rent_due_date = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else "" # 租金应收日
+ rent_payout_date = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "" # 租金收款日期
+ # 判断租金收款状态
+ if rent_rec.deposit_state == 'paid': # 已全额收款
+ rent_status = "已收"
+ elif rent_rec.deposit_state == 'no_paid': # 收部分
+ rent_status = "收部分"
+ else: # 未收
+ rent_status = "未收"
+
+ # --- 4.4 违约金信息 ---
+ penalty_amount = 0.0 # 违约金金额
+ penalty_collected = 0.0 # 违约金已收(全部历史)
+ penalty_uncollected = 0.0 # 违约金未收
+ penalty_status = "" # 违约金状态
+ penalty_start_date = "" # 违约金应收开始
+
+ if penalty_rec: # 如果有违约金记录
+ penalty_amount = round(penalty_rec.original_amount or 0.0, 2) # 违约金金额
+ penalty_collected = round(penalty_rec.delivered1 or 0.0, 2) # 违约金已收
+ penalty_uncollected = round(penalty_rec.submitted or 0.0, 2) # 违约金未收
+ penalty_start_date = penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "" # 应收开始日期
+ # 违约金状态(从 selection 取中文显示名)
+ penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
+ penalty_rec.deposit_state, penalty_rec.deposit_state)
+
+ # --- 4.5 累计统计 ---
+ total_collected += pay_amount # 累加本笔实收违约金
+
+ # --- 4.6 构建物业名称显示 ---
+ property_display = "" # 初始化物业名称
+ if contract and contract.lease_ids and contract.lease_ids[0].address_id: # 如果合同有租赁明细且有物业地址
+ property_display = { # 带跳转链接的物业名称
+ 'model': 'yuthon.property', # 跳转目标模型
+ 'id': contract.lease_ids[0].address_id.id, # 物业记录 ID
+ 'display_name': receivable.property_name or "", # 显示名称
+ }
+ else: # 没有物业地址时
+ property_display = receivable.property_name or "" # 纯文本显示
+
+ # --- 4.7 构建行数据(每笔付款一行) ---
rows.append({
- "序号": idx,
- "合同编号": {
- 'model': 'property.lease.contract',
- 'id': rec.contract_id.id,
- 'display_name': rec.contract_id.contract_code,
- } if rec.contract_id else "",
- "物业名称": {
- 'model': 'yuthon.property',
- 'id': rec.contract_id.lease_ids[0].address_id.id,
- 'display_name': rec.property_name,
- } if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "",
- "承租方名称": {
- 'model': 'property.tenant.information',
- 'id': rec.lessee_id.id,
- 'display_name': rec.lessee_id.name,
- } if rec.lessee_id else "",
- "金额": round(amount, 2),
- "应收金额": round((rec.original_amount or 0.0) - (rec.discount_amount or 0.0), 2),
- "违约计算时间(天)": billing_period,
- "欠租金额": round(rent_owed, 2),
- "每天违约金(元/天)": round(per_day, 2),
- "违约金金额": round(penalty_amount, 2),
- "总已收金额": round(collected, 2),
- "总未收": round(uncollected, 2),
- "当月收款时间": rec.payout_date.strftime("%Y-%m-%d") if rec.payout_date else "",
- "当月收款金额": round(collected, 2),
- "所属公司": rec.company_id.name if rec.company_id else "",
- "产权证号": rec.title_deed or "",
- "管理员": {
- 'model': 'res.users',
- 'id': rec.admin_id.id,
- 'display_name': rec.admin_id.name,
- } if rec.admin_id else "",
+ "租金ID": rent_rec.id if rent_rec else "", # 租金记录 ID
+ "租金应收日": rent_due_date, # 租金应收日期
+ "租金收款日期": rent_payout_date, # 租金收款日期
+ "租金金额": rent_amount, # 租金金额
+ "租金已收": rent_collected, # 租金已收
+ "租金未收": rent_uncollected, # 租金未收
+ "租金状态": rent_status, # 租金状态
+ "违约金金额": penalty_amount, # 违约金金额
+ "违约金已收": penalty_collected, # 违约金已收(全部历史)
+ "违约金未收": penalty_uncollected, # 违约金未收
+ "违约金状态": penalty_status, # 违约金状态
+ "违约金应收开始": penalty_start_date, # 违约金应收开始日期
+ "实收金额": pay_amount, # 本笔实收违约金金额
+ "收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "", # 本笔收款日期
+ "收款备注": line.remake1 or "", # 本笔收款备注
+ "收费区间": receivable.charging_range or "", # 收费区间
+ "备注": receivable.remark or "", # 备注
+ "合同编号": { # 带跳转链接的合同编号
+ 'model': 'property.lease.contract', # 跳转目标模型
+ 'id': contract.id, # 合同记录 ID
+ 'display_name': contract.contract_code, # 显示合同编号
+ } if contract else "", # 无合同时显示空
+ "物业名称": property_display, # 物业名称
+ "承租方名称": { # 带跳转链接的承租方名称
+ 'model': 'property.tenant.information', # 跳转目标模型
+ 'id': lessee.id, # 承租方记录 ID
+ 'display_name': lessee.name, # 显示承租方名称
+ } if lessee else "", # 无承租方时显示空
+ "所属公司": receivable.company_id.name if receivable.company_id else "", # 所属公司
})
+ # ========== 五、构建统计汇总数据 ==========
stats = [
- {'name': '记录总数', 'amount': len(rows)},
- {'name': '违约金总额', 'amount': round(total_penalty, 2)},
- {'name': '总已收', 'amount': round(total_collected, 2)},
- {'name': '总未收', 'amount': round(total_uncollected, 2)},
+ {'name': '记录总数', 'amount': len(rows)}, # 报表行数
+ {'name': '实收违约金', 'amount': round(total_collected, 2)}, # 本月实收违约金总额
]
- return {"rows": rows, "stats": stats}
+ # 输出调试日志
+ _logger.info("[PENALTY_MONTH_RECEIVED] ds=%s, de=%s, rows=%d, total=%.2f",
+ ds, de, len(rows), total_collected)
+
+ return {"rows": rows, "stats": stats} # 返回报表数据(行 + 统计)
diff --git a/yuthon_report/models/penalty_year_cum_report.py b/yuthon_report/models/penalty_year_cum_report.py
index f15dbfcac..0ffb48bd5 100644
--- a/yuthon_report/models/penalty_year_cum_report.py
+++ b/yuthon_report/models/penalty_year_cum_report.py
@@ -1,116 +1,239 @@
-import datetime
+import datetime # 日期时间处理模块,用于日期解析和计算
+import logging # 日志模块,用于调试输出
-from odoo import api, models
+from odoo import api, models # Odoo 核心 API:api 装饰器、models 模型基类
+
+_logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
class PenaltyYearCumReport(models.Model):
- _inherit = 'property.accounts.receivable'
+ _inherit = 'property.accounts.receivable' # 继承 property.accounts.receivable 模型,扩展报表方法
@api.model
def get_penalty_year_cum_report(self, params):
"""
- 违约金本年累计报表:
- - 筛选违约金记录(is_penalty=True 或 management_type_id 含"违约")
- - 本年内有收款:payout_date 在本年第一天到最后一天之间
- - 按合同分组,金额 = sum(penalty)
- - 字段:序号/合同编号/物业名称/承租方名称/金额/所属公司/管理员
+ 违约金本年累计报表
+
+ 核心逻辑:
+ 1. 数据源:receivable.modify.line(is_penalty=True, reconciliation_date在查询年内)
+ 2. 查询年范围:当年1月1日 到 当年12月31日(整年)
+ 3. 每笔付款单独一行,不合并,展示每笔的收款日期和金额
+ 4. 关联租金:反查模式(同合同+承租方,违约金.due_date == 租金.payout_date)
"""
- today = datetime.date.today()
- year_first = today.replace(month=1, day=1)
- year_last = today.replace(month=12, day=31)
- penalty_types = self.env['yuthon.management.type'].search([('name', 'ilike', '违约')])
+ # ========== 一、参数解析 ==========
+ date_start = params.get("date_start") # 查询开始日期(如 "2026-01-01 00:00:00")
+ date_end = params.get("date_end") # 查询结束日期(如 "2026-01-31 23:59:59")
- date_start = params.get("date_start")
- date_end = params.get("date_end")
- start_dt = datetime.datetime.fromisoformat(date_start).date() if date_start else year_first
- end_dt = datetime.datetime.fromisoformat(date_end).date() if date_end else year_last
+ # 没有时间范围则无数据
+ if not date_start or not date_end:
+ return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
- domain = [
- ('payout_date', '>=', start_dt),
- ('payout_date', '<=', end_dt),
- ('is_invalid', '!=', True),
- ]
+ # 从参数中解析年份,确定本年的完整时间范围
+ de = datetime.datetime.fromisoformat(date_end).date() # 解析查询结束日期
+ year = de.year # 取查询年份
+ # 本年累计:1月1日 到 12月31日(整年)
+ ds = datetime.date(year, 1, 1) # 当年1月1日
+ de = datetime.date(year, 12, 31) # 当年12月31日
- # 违约金过滤
- if penalty_types:
- domain += ['|', ('management_type_id', 'in', penalty_types.ids), ('is_penalty', '=', True)]
- else:
- domain.append(('is_penalty', '=', True))
+ # 其他筛选条件参数
+ keyword = params.get("keyword") # 关键字搜索
+ condition = params.get('condition') # 租赁类型
+ partner_id = params.get('partner_id') # 片区 ID
+ company_id = params.get('company_id') # 公司 ID
+ property_id = params.get('property_id') # 物业 ID
+ lessee_id = params.get('lessee_id') # 承租方 ID
+ contract_id = params.get('contract_id') # 合同 ID
- keyword = params.get("keyword")
- if keyword:
- domain += [
- '|', '|',
- ('contract_id.contract_code', 'ilike', keyword),
- ('property_name', 'ilike', keyword),
- ('lessee_id.name', 'ilike', keyword),
- ]
+ # ========== 二、搜索查询年内违约金付款记录 ==========
+ # 直接从 receivable.modify.line 查找本年实收的违约金
+ modify_lines = self.env['receivable.modify.line'].sudo().search([
+ ('is_penalty', '=', True), # 标记为违约金付款
+ ('reconciliation_date', '!=', False), # 有对账日期(已确认付款)
+ ('reconciliation_date', '>=', ds), # 对账日期 >= 当年1月1日
+ ('reconciliation_date', '<=', de), # 对账日期 <= 当年12月31日
+ ])
- condition = params.get('condition')
- partner_id = params.get('partner_id')
- company_id = params.get('company_id')
+ # 没有付款记录则直接返回空
+ if not modify_lines:
+ return {"rows": [], "stats": [{'name': '记录总数', 'amount': 0}, {'name': '实收违约金', 'amount': 0}]}
- if condition:
- domain.append(('tenure_type', '=', condition))
- if partner_id:
- domain.append(('precinct_id', '=', partner_id))
- if company_id:
- domain.append(('company_id', '=', company_id))
+ # ========== 三、构建关联索引 ==========
+ # 收集所有涉及的合同 ID,用于预查
+ contract_ids = set() # 合同 ID 集合
+ for line in modify_lines: # 遍历每条付款记录
+ rec = line.receivable_id # 获取关联的应收记录
+ if rec.contract_id: # 如果有关联合同
+ contract_ids.add(rec.contract_id.id) # 添加合同 ID 到集合
- receivables = self.search(domain, order="contract_id asc, due_date asc")
+ # 预查所有相关违约金记录(用于 source_receivable_id 为空时备用反查)
+ all_penalties = self.sudo().search([
+ ('management_type_id.name', '=', '违约金'), # 类型为违约金
+ ('is_invalid', '!=', True), # 排除已作废
+ ('contract_id', 'in', list(contract_ids)), # 在涉及的合同范围内
+ ])
- # 按合同分组,金额 = sum(penalty)
- contract_map = {}
- for rec in receivables:
- cid = rec.contract_id.id if rec.contract_id else 0
- if cid not in contract_map:
- contract_map[cid] = {
- 'info': rec,
- 'total_penalty': 0.0,
- 'total_receivable': 0.0,
+ # ========== 四、构建报表行(每笔付款单独一行) ==========
+ rows = [] # 报表行数据列表
+ total_collected = 0.0 # 本年实收违约金总计
+
+ for line in modify_lines: # 遍历每条违约金付款记录(不合并)
+ receivable = line.receivable_id # 获取关联的应收记录
+ pay_amount = round(line.amount or 0.0, 2) # 本笔收款金额
+ pay_date = line.reconciliation_date # 本笔收款日期
+
+ # 收款金额为0或负数则跳过
+ if pay_amount <= 0:
+ continue
+
+ # --- 4.1 内存过滤:应用关键字和条件筛选 ---
+ contract = receivable.contract_id # 获取关联合同
+ lessee = receivable.lessee_id # 获取关联承租方
+
+ # 关键字过滤
+ if keyword: # 如果有关键字条件
+ kw = keyword.lower() # 转小写用于匹配
+ match = False # 匹配标志
+ if contract and contract.contract_code and kw in contract.contract_code.lower(): # 合同编号匹配
+ match = True
+ if receivable.property_name and kw in receivable.property_name.lower(): # 物业名称匹配
+ match = True
+ if lessee and lessee.name and kw in lessee.name.lower(): # 承租方名称匹配
+ match = True
+ if not match: # 三项都不匹配
+ continue # 跳过该记录
+
+ # 租赁类型过滤
+ if condition and receivable.tenure_type != condition: # 租赁类型不匹配
+ continue
+
+ # 片区过滤
+ if partner_id and receivable.precinct_id.id != partner_id: # 片区不匹配
+ continue
+
+ # 公司过滤
+ if company_id and receivable.company_id.id != company_id: # 公司不匹配
+ continue
+
+ # 物业过滤
+ if property_id and property_id not in (receivable.property_ids.ids or []): # 物业不匹配
+ continue
+
+ # 承租方过滤
+ if lessee_id and (not lessee or lessee.id != lessee_id): # 承租方不匹配
+ continue
+
+ # 合同过滤
+ if contract_id and (not contract or contract.id != contract_id): # 合同不匹配
+ continue
+
+ # --- 4.2 判断 receivable 类型并关联租金 ---
+ # receivable 可能是违约金记录或租金记录
+ is_penalty_rec = receivable.management_type_id.name == '违约金' # 是否为违约金记录
+
+ rent_rec = None # 关联的租金记录
+ penalty_rec = None # 关联的违约金记录
+
+ if is_penalty_rec: # 如果 receivable 本身就是违约金记录
+ penalty_rec = receivable # 直接使用
+ # 通过 source_receivable_id 直接关联到原始租金记录
+ rent_rec = penalty_rec.source_receivable_id # 直接取违约金中存储的原始应收(租金)记录
+ else: # receivable 是租金记录
+ rent_rec = receivable # 直接使用
+
+ # --- 4.3 租金信息 ---
+ rent_amount = 0.0 # 租金金额
+ rent_collected = 0.0 # 租金已收
+ rent_uncollected = 0.0 # 租金未收
+ rent_status = "" # 租金状态
+ rent_due_date = "" # 租金应收日
+ rent_payout_date = "" # 租金收款日期
+
+ if rent_rec: # 如果找到了关联租金记录
+ rent_amount = round((rent_rec.original_amount or 0.0) - (rent_rec.discount_amount or 0.0), 2) # 租金金额 = 原金额 - 折扣
+ rent_collected = round(rent_rec.delivered1 or 0.0, 2) # 租金已收
+ rent_uncollected = round(rent_rec.submitted or 0.0, 2) # 租金未收
+ rent_due_date = rent_rec.due_date.strftime("%Y-%m-%d") if rent_rec.due_date else "" # 租金应收日
+ rent_payout_date = rent_rec.payout_date.strftime("%Y-%m-%d") if rent_rec.payout_date else "" # 租金收款日期
+ # 判断租金收款状态
+ if rent_rec.deposit_state == 'paid': # 已全额收款
+ rent_status = "已收"
+ elif rent_rec.deposit_state == 'no_paid': # 收部分
+ rent_status = "收部分"
+ else: # 未收
+ rent_status = "未收"
+
+ # --- 4.4 违约金信息 ---
+ penalty_amount = 0.0 # 违约金金额
+ penalty_collected = 0.0 # 违约金已收(全部历史)
+ penalty_uncollected = 0.0 # 违约金未收
+ penalty_status = "" # 违约金状态
+ penalty_start_date = "" # 违约金应收开始
+
+ if penalty_rec: # 如果有违约金记录
+ penalty_amount = round(penalty_rec.original_amount or 0.0, 2) # 违约金金额
+ penalty_collected = round(penalty_rec.delivered1 or 0.0, 2) # 违约金已收
+ penalty_uncollected = round(penalty_rec.submitted or 0.0, 2) # 违约金未收
+ penalty_start_date = penalty_rec.due_date.strftime("%Y-%m-%d") if penalty_rec.due_date else "" # 应收开始日期
+ # 违约金状态(从 selection 取中文显示名)
+ penalty_status = dict(penalty_rec._fields['deposit_state'].selection).get(
+ penalty_rec.deposit_state, penalty_rec.deposit_state)
+
+ # --- 4.5 累计统计 ---
+ total_collected += pay_amount # 累加本笔实收违约金
+
+ # --- 4.6 构建物业名称显示 ---
+ property_display = "" # 初始化物业名称
+ if contract and contract.lease_ids and contract.lease_ids[0].address_id: # 如果合同有租赁明细且有物业地址
+ property_display = { # 带跳转链接的物业名称
+ 'model': 'yuthon.property', # 跳转目标模型
+ 'id': contract.lease_ids[0].address_id.id, # 物业记录 ID
+ 'display_name': receivable.property_name or "", # 显示名称
}
- contract_map[cid]['total_penalty'] += rec.penalty or 0.0
- contract_map[cid]['total_receivable'] += (rec.original_amount or 0.0) - (rec.discount_amount or 0.0)
-
- rows = []
- total_amount = 0.0
- for idx, (cid, data) in enumerate(contract_map.items(), start=1):
- rec = data['info']
- contract = rec.contract_id
- amount = round(data['total_penalty'], 2)
- total_amount += amount
+ else: # 没有物业地址时
+ property_display = receivable.property_name or "" # 纯文本显示
+ # --- 4.7 构建行数据(每笔付款一行) ---
rows.append({
- "序号": idx,
- "合同编号": {
- 'model': 'property.lease.contract',
- 'id': contract.id,
- 'display_name': contract.contract_code,
- } if contract else "",
- "物业名称": {
- 'model': 'yuthon.property',
- 'id': rec.contract_id.lease_ids[0].address_id.id,
- 'display_name': rec.property_name,
- } if rec.contract_id and rec.contract_id.lease_ids and rec.contract_id.lease_ids[0].address_id else rec.property_name or "",
- "承租方名称": {
- 'model': 'property.tenant.information',
- 'id': rec.lessee_id.id,
- 'display_name': rec.lessee_id.name,
- } if rec.lessee_id else "",
- "金额": amount,
- "应收金额": round(data['total_receivable'], 2),
- "所属公司": rec.company_id.name if rec.company_id else "",
- "管理员": {
- 'model': 'res.users',
- 'id': rec.admin_id.id,
- 'display_name': rec.admin_id.name,
- } if rec.admin_id else "",
+ "租金ID": rent_rec.id if rent_rec else "", # 租金记录 ID
+ "租金应收日": rent_due_date, # 租金应收日期
+ "租金收款日期": rent_payout_date, # 租金收款日期
+ "租金金额": rent_amount, # 租金金额
+ "租金已收": rent_collected, # 租金已收
+ "租金未收": rent_uncollected, # 租金未收
+ "租金状态": rent_status, # 租金状态
+ "违约金金额": penalty_amount, # 违约金金额
+ "违约金已收": penalty_collected, # 违约金已收(全部历史)
+ "违约金未收": penalty_uncollected, # 违约金未收
+ "违约金状态": penalty_status, # 违约金状态
+ "违约金应收开始": penalty_start_date, # 违约金应收开始日期
+ "实收金额": pay_amount, # 本笔实收违约金金额
+ "收款日期": pay_date.strftime("%Y-%m-%d") if pay_date else "", # 本笔收款日期
+ "收款备注": line.remake1 or "", # 本笔收款备注
+ "收费区间": receivable.charging_range or "", # 收费区间
+ "备注": receivable.remark or "", # 备注
+ "合同编号": { # 带跳转链接的合同编号
+ 'model': 'property.lease.contract', # 跳转目标模型
+ 'id': contract.id, # 合同记录 ID
+ 'display_name': contract.contract_code, # 显示合同编号
+ } if contract else "", # 无合同时显示空
+ "物业名称": property_display, # 物业名称
+ "承租方名称": { # 带跳转链接的承租方名称
+ 'model': 'property.tenant.information', # 跳转目标模型
+ 'id': lessee.id, # 承租方记录 ID
+ 'display_name': lessee.name, # 显示承租方名称
+ } if lessee else "", # 无承租方时显示空
+ "所属公司": receivable.company_id.name if receivable.company_id else "", # 所属公司
})
+ # ========== 五、构建统计汇总数据 ==========
stats = [
- {'name': '合同总数', 'amount': len(rows)},
- {'name': '违约金总额', 'amount': round(total_amount, 2)},
+ {'name': '记录总数', 'amount': len(rows)}, # 报表行数
+ {'name': '实收违约金', 'amount': round(total_collected, 2)}, # 本年实收违约金总额
]
- return {"rows": rows, "stats": stats}
+ # 输出调试日志
+ _logger.info("[PENALTY_YEAR_CUM] year=%d, ds=%s, de=%s, rows=%d, total=%.2f",
+ year, ds, de, len(rows), total_collected)
+
+ return {"rows": rows, "stats": stats} # 返回报表数据(行 + 统计)
diff --git a/yuthon_report/models/rent_area_report.py b/yuthon_report/models/rent_area_report.py
new file mode 100644
index 000000000..cdc3a0105
--- /dev/null
+++ b/yuthon_report/models/rent_area_report.py
@@ -0,0 +1,235 @@
+import datetime
+import logging
+
+from odoo import api, models
+
+_logger = logging.getLogger(__name__)
+
+
+class RentAreaReport(models.Model):
+ _name = 'rent.area.report'
+ _description = '可租面积报表'
+ _auto = False
+
+ @api.model
+ def get_rent_area_report(self, params):
+ """可租面积报表数据源。
+
+ 核心逻辑:
+ 1. 数据源为 manage.information.line(管理信息明细行)
+ 2. 日期过滤:生效日期 <= 查询日期 <= 失效日期(失效日期为空表示至今有效)
+ 3. 可租面积直接取 manage.information.line.rent_area
+ 4. 产权证号、产权地址从 title.information.line 取(也按日期范围匹配)
+ 5. 片区管理员取 precinct_id.display_name
+ 6. 合同编号、承租方从 property.lease.contract.line 反查合同获取
+ 7. 支持按物业名称(property_id)、承租方(lessee_id)、合同编号(contract_id)搜索
+ """
+
+ # ========== 一、参数解析 ==========
+ date_start = params.get("date_start")
+ date_end = params.get("date_end")
+ # 单日期模式,优先取 date_start
+ query_date_str = date_start or date_end
+
+ keyword = params.get("keyword")
+ condition = params.get('condition')
+ partner_id = params.get('partner_id')
+ company_id = params.get('company_id')
+ # 前端侧边栏下拉选择器传入的 ID
+ property_id = params.get('property_id')
+ lessee_id = params.get('lessee_id')
+ contract_id = params.get('contract_id')
+
+ # ========== 二、查询日期解析 ==========
+ query_date = None
+ if query_date_str:
+ try:
+ # 截断时间后缀,如 "2024-11-01 00:00:00" -> "2024-11-01"
+ query_date = datetime.datetime.fromisoformat(query_date_str.split(' ')[0]).date()
+ except Exception:
+ _logger.warning("[RENT_AREA] 日期解析失败: %s", query_date_str)
+
+ # 没有查询日期时不做时间过滤(返回所有记录)
+ domain = [('yuthon_property_id', '!=', False)]
+
+ # 日期过滤:生效日期 <= 查询日期,且(失效日期 >= 查询日期 或 失效日期为空)
+ if query_date:
+ domain += [
+ ('start_time', '<=', query_date),
+ '|', ('end_date', '>=', query_date), ('end_date', '=', False),
+ ]
+
+ # 其他筛选条件
+ if condition:
+ domain.append(('tenure_type', '=', condition))
+ if partner_id:
+ domain.append(('precinct_id', '=', partner_id))
+ if company_id:
+ domain.append(('company_id', '=', company_id))
+ # 物业名称下拉选择器:直接过滤 yuthon_property_id
+ if property_id:
+ domain.append(('yuthon_property_id', '=', int(property_id)))
+
+ # ========== 三、查询管理信息明细行 ==========
+ manage_lines = self.env['manage.information.line'].sudo().search(domain)
+
+ # ========== 四、批量预取合同信息 ==========
+ # 避免每条记录单独查询,一次性查出所有物业对应的合同
+ # 关联路径:manage.information.line → yuthon_property_id → yuthon.property
+ # ← property.lease.contract.line(address_id) → lease_id → property.lease.contract
+ property_ids = list({line.yuthon_property_id.id for line in manage_lines if line.yuthon_property_id})
+ # 合同映射:property_id → 合同信息列表
+ contract_map = {}
+ if property_ids:
+ contract_lines = self.env['property.lease.contract.line'].sudo().search([
+ ('address_id', 'in', property_ids),
+ ])
+ for cl in contract_lines:
+ contract = cl.lease_id
+ if not contract:
+ continue
+ pid = cl.address_id.id
+ # 同一物业可能有多份合同,用列表存储
+ if pid not in contract_map:
+ contract_map[pid] = []
+ contract_map[pid].append({
+ 'contract_id': contract.id,
+ 'contract_code': contract.contract_code or "",
+ 'lessee_id': contract.lessee_id.id if contract.lessee_id else False,
+ 'lessee_name': contract.lessee_id.name if contract.lessee_id else "",
+ 'actual_start_time': contract.actual_start_time,
+ 'actual_end_time': contract.actual_end_time,
+ 'is_contract': contract.is_contract,
+ 'total_area': contract.total_area or 0.0,
+ })
+
+ # ========== 五、承租方/合同编号过滤 ==========
+ # 前端传入 lessee_id 或 contract_id 时,需要根据合同信息过滤
+ if lessee_id or contract_id:
+ allowed_property_ids = set()
+ for pid, contracts in contract_map.items():
+ for c in contracts:
+ # 承租方匹配
+ if lessee_id and c['lessee_id'] != int(lessee_id):
+ continue
+ # 合同编号匹配
+ if contract_id and c['contract_id'] != int(contract_id):
+ continue
+ allowed_property_ids.add(pid)
+ break
+ # 过滤 manage_lines,只保留符合条件的物业
+ manage_lines = manage_lines.filtered(
+ lambda l: l.yuthon_property_id.id in allowed_property_ids
+ )
+
+ # ========== 六、构建报表行 ==========
+ rows = []
+ total_rent_area = 0.0
+
+ for idx, line in enumerate(manage_lines, start=1):
+ property_rec = line.yuthon_property_id
+ if not property_rec:
+ continue
+
+ # --- 6.1 取合同信息(查询日期范围内生效的合同) ---
+ contract_code = ""
+ contract_record_id = False
+ lessee_name = ""
+ lessee_record_id = False
+ contracts = contract_map.get(property_rec.id, [])
+ if contracts:
+ # 优先取查询日期范围内生效的主合同
+ active_contracts = contracts
+ if query_date:
+ # 过滤:合同生效日期 <= 查询日期 且 合同失效日期 >= 查询日期
+ active_contracts = [
+ c for c in contracts
+ if c['actual_start_time'] and c['actual_start_time'] <= query_date
+ and (not c['actual_end_time'] or c['actual_end_time'] >= query_date)
+ ]
+ # 优先取主合同(is_contract=True)
+ main_contracts = [c for c in active_contracts if c['is_contract']]
+ target = main_contracts[0] if main_contracts else (active_contracts[0] if active_contracts else None)
+ if target:
+ contract_code = target['contract_code']
+ contract_record_id = target['contract_id']
+ lessee_name = target['lessee_name']
+ lessee_record_id = target['lessee_id']
+
+ # --- 6.2 关键字过滤(物业名称、承租方、合同编号) ---
+ if keyword:
+ match = False
+ kw = keyword.lower()
+ # 物业名称
+ if property_rec.name and kw in property_rec.name.lower():
+ match = True
+ # 承租方
+ if lessee_name and kw in lessee_name.lower():
+ match = True
+ # 合同编号
+ if contract_code and kw in contract_code.lower():
+ match = True
+ if not match:
+ continue
+
+ # --- 6.3 取产权信息(产权证号、产权地址) ---
+ title_certificate = ""
+ property_address = ""
+ if property_rec.title_information_ids:
+ valid_titles = property_rec.title_information_ids
+ if query_date:
+ valid_titles = valid_titles.filtered(
+ lambda t: t.start_time and t.start_time <= query_date and
+ (not t.end_date or t.end_date >= query_date)
+ )
+ if valid_titles:
+ # 按生效日期降序取最新的
+ latest_title = valid_titles.sorted('start_time', reverse=True)[0]
+ title_certificate = latest_title.title_certificate or ""
+ if latest_title.property_message_id:
+ property_address = latest_title.property_message_id.name or ""
+
+ # --- 6.4 片区管理员 ---
+ admin_display = ""
+ if line.precinct_id:
+ admin_display = line.precinct_id.display_name or ""
+
+ # --- 6.5 累计统计 ---
+ rent_area = line.rent_area or 0.0
+ total_rent_area += rent_area
+
+ # --- 6.6 构建行数据(物业名称、合同编号、承租方支持穿透) ---
+ rows.append({
+ "序号": idx,
+ "物业id": property_rec.id,
+ "物业名称": {
+ 'model': 'yuthon.property',
+ 'id': property_rec.id,
+ 'display_name': property_rec.name or "",
+ } if property_rec.name else "",
+ "合同编号": {
+ 'model': 'property.lease.contract',
+ 'id': contract_record_id,
+ 'display_name': contract_code,
+ } if contract_record_id else contract_code or "",
+ "承租方": {
+ 'model': 'property.tenant.information',
+ 'id': lessee_record_id,
+ 'display_name': lessee_name,
+ } if lessee_record_id else lessee_name or "",
+ "产权证号": title_certificate,
+ "产权地址": property_address,
+ "可租面积": round(rent_area, 4),
+ "片区管理员": admin_display,
+ })
+
+ # ========== 七、构建统计汇总数据 ==========
+ stats = [
+ {'name': '记录总数', 'amount': len(rows)},
+ {'name': '可租面积合计', 'amount': round(total_rent_area, 4)},
+ ]
+
+ _logger.info("[RENT_AREA] query_date=%s, lines=%d, rows=%d, total_area=%.4f",
+ query_date, len(manage_lines), len(rows), total_rent_area)
+
+ return {"rows": rows, "stats": stats}
diff --git a/yuthon_report/models/rental_rate_report.py b/yuthon_report/models/rental_rate_report.py
new file mode 100644
index 000000000..25c17734c
--- /dev/null
+++ b/yuthon_report/models/rental_rate_report.py
@@ -0,0 +1,249 @@
+import datetime
+import logging
+
+from odoo import api, models
+
+_logger = logging.getLogger(__name__)
+
+
+class RentalRateReport(models.Model):
+ _name = 'rental.rate.report'
+ _description = '出租率报表'
+ _auto = False
+
+ @api.model
+ def get_rental_rate_report(self, params):
+ """出租率报表数据源。
+
+ 核心逻辑:
+ 1. 数据源为 manage.information.line(管理信息明细行),取可租面积
+ 2. 通过 property.lease.contract.line 反查执行中合同,取物业已租面积和合同已租面积
+ 3. 物业已租面积:contract_line.property_area(compute 字段)
+ 4. 合同已租面积:contract.total_area(合同总面积)
+ 5. 出租率 = 物业已租面积 / 可租面积 * 100
+ 6. 产权证号、产权地址从 title.information.line 取
+ 7. 片区管理员从 precinct_id.display_name 取
+ 8. 支持按物业名称(property_id)、承租方(lessee_id)、合同编号(contract_id)搜索
+ """
+
+ # ========== 一、参数解析 ==========
+ date_start = params.get("date_start")
+ date_end = params.get("date_end")
+ # 单日期模式,优先取 date_start
+ query_date_str = date_start or date_end
+
+ keyword = params.get("keyword")
+ condition = params.get('condition')
+ partner_id = params.get('partner_id')
+ company_id = params.get('company_id')
+ # 前端侧边栏下拉选择器传入的 ID
+ property_id = params.get('property_id')
+ lessee_id = params.get('lessee_id')
+ contract_id = params.get('contract_id')
+
+ # ========== 二、查询日期解析 ==========
+ query_date = None
+ if query_date_str:
+ try:
+ # 截断时间后缀
+ query_date = datetime.datetime.fromisoformat(query_date_str.split(' ')[0]).date()
+ except Exception:
+ _logger.warning("[RENTAL_RATE] 日期解析失败: %s", query_date_str)
+
+ # ========== 三、查询管理信息明细行 ==========
+ domain = [('yuthon_property_id', '!=', False)]
+
+ # 日期过滤:生效日期 <= 查询日期,且(失效日期 >= 查询日期 或 失效日期为空)
+ if query_date:
+ domain += [
+ ('start_time', '<=', query_date),
+ '|', ('end_date', '>=', query_date), ('end_date', '=', False),
+ ]
+
+ # 其他筛选条件
+ if condition:
+ domain.append(('tenure_type', '=', condition))
+ if partner_id:
+ domain.append(('precinct_id', '=', partner_id))
+ if company_id:
+ domain.append(('company_id', '=', company_id))
+ # 物业名称下拉选择器
+ if property_id:
+ domain.append(('yuthon_property_id', '=', int(property_id)))
+
+ manage_lines = self.env['manage.information.line'].sudo().search(domain)
+
+ # ========== 四、批量预取合同信息(计算已租面积) ==========
+ property_ids = list({line.yuthon_property_id.id for line in manage_lines if line.yuthon_property_id})
+ # 映射:property_id → 物业已租面积总和(property_area 累加)
+ property_leased_map = {}
+ # 映射:property_id → 合同已租面积总和(total_area 累加)
+ contract_leased_map = {}
+ # 映射:property_id → 合同信息列表(用于搜索过滤)
+ contract_info_map = {}
+ if property_ids:
+ # 只取执行中的合同
+ contract_domain = [
+ ('address_id', 'in', property_ids),
+ ('lease_id.contact_state', '=', 'ing'), # 只取执行中的合同
+ ]
+ contract_lines = self.env['property.lease.contract.line'].sudo().search(contract_domain)
+ for cl in contract_lines:
+ contract = cl.lease_id
+ if not contract:
+ continue
+ pid = cl.address_id.id
+ # 累加物业已租面积(property_area)
+ if pid not in property_leased_map:
+ property_leased_map[pid] = 0.0
+ property_leased_map[pid] += cl.property_area or 0.0
+ # 累加合同已租面积(total_area)
+ if pid not in contract_leased_map:
+ contract_leased_map[pid] = 0.0
+ contract_leased_map[pid] += contract.total_area or 0.0
+ # 合同信息(用于搜索过滤)
+ if pid not in contract_info_map:
+ contract_info_map[pid] = []
+ contract_info_map[pid].append({
+ 'contract_id': contract.id,
+ 'contract_code': contract.contract_code or "",
+ 'lessee_id': contract.lessee_id.id if contract.lessee_id else False,
+ 'lessee_name': contract.lessee_id.name if contract.lessee_id else "",
+ 'is_contract': contract.is_contract,
+ 'total_area': contract.total_area or 0.0,
+ 'property_area': cl.property_area or 0.0,
+ })
+
+ # ========== 五、承租方/合同编号过滤 ==========
+ if lessee_id or contract_id:
+ allowed_property_ids = set()
+ for pid, contracts in contract_info_map.items():
+ for c in contracts:
+ if lessee_id and c['lessee_id'] != int(lessee_id):
+ continue
+ if contract_id and c['contract_id'] != int(contract_id):
+ continue
+ allowed_property_ids.add(pid)
+ break
+ manage_lines = manage_lines.filtered(
+ lambda l: l.yuthon_property_id.id in allowed_property_ids
+ )
+
+ # ========== 六、构建报表行 ==========
+ rows = []
+ total_rent_area = 0.0
+ total_property_leased = 0.0
+ total_contract_leased = 0.0
+
+ for idx, line in enumerate(manage_lines, start=1):
+ property_rec = line.yuthon_property_id
+ if not property_rec:
+ continue
+
+ # --- 6.1 可租面积 ---
+ rent_area = line.rent_area or 0.0
+ # --- 6.2 物业已租面积(从合同 property_area 累加) ---
+ property_leased = property_leased_map.get(property_rec.id, 0.0)
+ # --- 6.3 合同已租面积(从合同 total_area 累加) ---
+ contract_leased = contract_leased_map.get(property_rec.id, 0.0)
+ # --- 6.4 出租率 = 物业已租面积 / 可租面积 ---
+ rental_rate = round((property_leased / rent_area * 100) if rent_area > 0 else 0, 2)
+
+ # --- 6.5 取合同信息(关键字过滤用) ---
+ contract_code = ""
+ contract_record_id = False
+ lessee_name = ""
+ lessee_record_id = False
+ contracts = contract_info_map.get(property_rec.id, [])
+ if contracts:
+ # 优先取主合同
+ main_contracts = [c for c in contracts if c['is_contract']]
+ target = main_contracts[0] if main_contracts else (contracts[0] if contracts else None)
+ if target:
+ contract_code = target['contract_code']
+ contract_record_id = target['contract_id']
+ lessee_name = target['lessee_name']
+ lessee_record_id = target['lessee_id']
+
+ # --- 6.6 关键字过滤(物业名称、承租方、合同编号) ---
+ if keyword:
+ match = False
+ kw = keyword.lower()
+ if property_rec.name and kw in property_rec.name.lower():
+ match = True
+ if lessee_name and kw in lessee_name.lower():
+ match = True
+ if contract_code and kw in contract_code.lower():
+ match = True
+ if not match:
+ continue
+
+ # --- 6.7 取产权信息(产权证号、产权地址) ---
+ title_certificate = ""
+ property_address = ""
+ if property_rec.title_information_ids:
+ valid_titles = property_rec.title_information_ids
+ if query_date:
+ valid_titles = valid_titles.filtered(
+ lambda t: t.start_time and t.start_time <= query_date and
+ (not t.end_date or t.end_date >= query_date)
+ )
+ if valid_titles:
+ latest_title = valid_titles.sorted('start_time', reverse=True)[0]
+ title_certificate = latest_title.title_certificate or ""
+ if latest_title.property_message_id:
+ property_address = latest_title.property_message_id.name or ""
+
+ # --- 6.8 片区管理员 ---
+ admin_display = ""
+ if line.precinct_id:
+ admin_display = line.precinct_id.display_name or ""
+
+ # --- 6.9 累计统计 ---
+ total_rent_area += rent_area
+ total_property_leased += property_leased
+ total_contract_leased += contract_leased
+
+ # --- 6.10 构建行数据(物业名称、合同编号、承租方支持穿透) ---
+ rows.append({
+ "序号": idx,
+ "物业id": property_rec.id,
+ "物业名称": {
+ 'model': 'yuthon.property',
+ 'id': property_rec.id,
+ 'display_name': property_rec.name or "",
+ } if property_rec.name else "",
+ "合同编号": {
+ 'model': 'property.lease.contract',
+ 'id': contract_record_id,
+ 'display_name': contract_code,
+ } if contract_record_id else contract_code or "",
+ "承租方": {
+ 'model': 'property.tenant.information',
+ 'id': lessee_record_id,
+ 'display_name': lessee_name,
+ } if lessee_record_id else lessee_name or "",
+ "产权证号": title_certificate,
+ "产权地址": property_address,
+ "可租面积": round(rent_area, 4),
+ "物业已租面积": round(property_leased, 4),
+ "合同已租面积": round(contract_leased, 4),
+ "出租率": rental_rate,
+ "片区管理员": admin_display,
+ })
+
+ # ========== 七、构建统计汇总数据 ==========
+ # 整体出租率 = 物业已租面积合计 / 可租面积合计
+ overall_rate = round((total_property_leased / total_rent_area * 100) if total_rent_area > 0 else 0, 2)
+ stats = [
+ {'name': '记录总数', 'amount': len(rows)},
+ {'name': '可租面积合计', 'amount': round(total_rent_area, 4)},
+ {'name': '物业已租面积合计', 'amount': round(total_property_leased, 4)},
+ {'name': '合同已租面积合计', 'amount': round(total_contract_leased, 4)},
+ {'name': '整体出租率', 'amount': overall_rate},
+ ]
+
+ _logger.info("[RENTAL_RATE] query_date=%s, rows=%d, rent=%.4f, property_leased=%.4f, contract_leased=%.4f, rate=%.2f%%",
+ query_date, len(rows), total_rent_area, total_property_leased, total_contract_leased, overall_rate)
+
+ return {"rows": rows, "stats": stats}
diff --git a/yuthon_report/models/yuthon_dashboard.py b/yuthon_report/models/yuthon_dashboard.py
index 335db444e..48672aac3 100644
--- a/yuthon_report/models/yuthon_dashboard.py
+++ b/yuthon_report/models/yuthon_dashboard.py
@@ -1,9 +1,12 @@
# -*- coding: utf-8 -*-
import datetime
import calendar
+import logging
from odoo import api, models
+_logger = logging.getLogger(__name__)
+
class YuthonDashboard(models.Model):
_name = 'yuthon.dashboard'
@@ -62,7 +65,8 @@ class YuthonDashboard(models.Model):
elif stats:
return stats[0].get('amount', 0.0)
return 0.0
- except Exception:
+ except Exception as e:
+ _logger.error("[DASHBOARD_SAFE_GET_ERROR] model=%s method=%s error=%s", model_name, method_name, e, exc_info=True)
return 0.0
# 全月范围参数(用于 dateMode="range" 的报表)
@@ -98,10 +102,28 @@ class YuthonDashboard(models.Model):
cum_income = safe_get('receivable.modify.line', 'get_cum_income_report', 1)
# ==================== 二、其他收入板块 ====================
- # 违约金
- penalty_cum_arrears = safe_get('property.accounts.receivable', 'get_penalty_arrears_report', 1) # 违约金总额
- penalty_month_received = safe_get('property.accounts.receivable', 'get_penalty_month_received_report', 2) # 总已收
- penalty_year_cum = safe_get('property.accounts.receivable', 'get_penalty_year_cum_report', 1) # 违约金总额
+ # 违约金:调用三个独立报表方法
+ # 累计欠收:stats = [记录总数, 违约金总额, 违约金已收, 违约金未收] → 取 index 3(违约金未收)
+ # 时间过滤与看板一致,直接用 base_params(月初1号当天)
+ penalty_cum_arrears = safe_get(
+ 'property.accounts.receivable', 'get_penalty_arrears_report', 3,
+ overrides=base_params
+ )
+ _logger.info("[DASHBOARD_PENALTY_ARREARS] base_params=%s, value=%s", base_params, penalty_cum_arrears)
+ # 本月实收:stats = [记录总数, 实收违约金] → 取 index 1
+ penalty_month_received = safe_get(
+ 'property.accounts.receivable', 'get_penalty_month_received_report', 1,
+ overrides=range_params
+ )
+ # 本年累计:stats = [记录总数, 实收违约金] → 取 index 1,范围 1月1日 到 查询月末
+ penalty_year_cum_params = dict(base_params)
+ if has_date:
+ y = query_date.year
+ penalty_year_cum_params['date_start'] = f"{y}-01-01 00:00:00"
+ penalty_year_cum = safe_get(
+ 'property.accounts.receivable', 'get_penalty_year_cum_report', 1,
+ overrides=penalty_year_cum_params
+ )
# 停车费
parking_month_receivable = safe_get('property.accounts.receivable', 'get_parking_fee_current_month_receivable', 0)
@@ -184,11 +206,14 @@ class YuthonDashboard(models.Model):
'title': '二、其他收入',
'cards': [
{'label': '违约金·累计欠收', 'value': penalty_cum_arrears, 'is_negative': True,
- 'action': 'yuthon_report.action_penalty_arrears_report', 'action_context': make_ctx()},
+ 'action': 'yuthon_report.action_penalty_arrears_report',
+ 'action_context': make_ctx({'date_end': base_params.get('date_end')})},
{'label': '违约金·本月实收', 'value': penalty_month_received, 'is_negative': False,
- 'action': 'yuthon_report.action_penalty_month_received_report', 'action_context': make_ctx()},
+ 'action': 'yuthon_report.action_penalty_month_received_report',
+ 'action_context': make_ctx({'date_start': range_params.get('date_start'), 'date_end': range_params.get('date_end')})},
{'label': '违约金·本年累计', 'value': penalty_year_cum, 'is_negative': False,
- 'action': 'yuthon_report.action_penalty_year_cum_report', 'action_context': make_ctx()},
+ 'action': 'yuthon_report.action_penalty_year_cum_report',
+ 'action_context': make_ctx({'date_start': penalty_year_cum_params.get('date_start'), 'date_end': penalty_year_cum_params.get('date_end')})},
{'label': '停车费·本月应收', 'value': parking_month_receivable, 'is_negative': False,
'action': 'yuthon_report.action_parking_fee_current_month_receivable', 'action_context': make_ctx()},
{'label': '停车费·本月实收', 'value': parking_month_received, 'is_negative': False,
@@ -349,49 +374,50 @@ class YuthonDashboard(models.Model):
return {'cards': [], 'row_groups': [], 'explanations': []}
def _get_area_data(self, base_params, query_date):
- """面积/比率板块"""
+ """面积/比率板块
+ 使用子报表方法获取汇总数据,确保看板卡片值与报表一致
+ 已租面积拆为两个卡片:物业已租面积、合同已租面积
+ """
try:
- domain = [('yuthon_property_id', '!=', False)]
- company_id = base_params.get('company_id')
- partner_id = base_params.get('partner_id')
- if company_id:
- domain.append(('company_id', '=', int(company_id)))
- if partner_id:
- domain.append(('precinct_id', '=', int(partner_id)))
+ # 管理面积:调用管理面积报表
+ management_result = self.env['management.area.report'].get_management_area_report(base_params)
+ management_stats = management_result.get('stats', [])
+ total_management = management_stats[1].get('amount', 0.0) if len(management_stats) > 1 else 0.0
- lines = self.env['manage.information.line'].sudo().search(domain)
+ # 可租面积:调用可租面积报表
+ rent_result = self.env['rent.area.report'].get_rent_area_report(base_params)
+ rent_stats = rent_result.get('stats', [])
+ total_rent = rent_stats[1].get('amount', 0.0) if len(rent_stats) > 1 else 0.0
- total_management = sum((l.management_area or 0.0) for l in lines)
- total_rent = sum((l.rent_area or 0.0) for l in lines)
- total_leased = 0.0
- total_contract_area = 0.0
+ # 已租面积:调用已租面积报表(返回物业已租面积和合同已租面积两个汇总)
+ leased_result = self.env['leased.area.report'].get_leased_area_report(base_params)
+ leased_stats = leased_result.get('stats', [])
+ # stats[1] = 物业已租面积合计, stats[2] = 合同已租面积合计
+ total_property_leased = leased_stats[1].get('amount', 0.0) if len(leased_stats) > 1 else 0.0
+ total_contract_leased = leased_stats[2].get('amount', 0.0) if len(leased_stats) > 2 else 0.0
- for line in lines:
- if line.yuthon_property_id:
- contract_lines = self.env['property.contract.line'].sudo().search(
- [('yuthon_property_id', '=', line.yuthon_property_id.id)], limit=1)
- if contract_lines and contract_lines.lease_contract_id:
- total_contract_area += contract_lines.lease_contract_id.total_area or 0.0
-
- # 出租率:已租面积 / 可租面积
- rental_rate = round((total_rent / total_management * 100) if total_management > 0 else 0, 2)
+ # 出租率:物业已租面积 / 可租面积
+ rental_rate = round((total_property_leased / total_rent * 100) if total_rent > 0 else 0, 2)
cards = [
{'label': '管理面积(㎡)', 'value': round(total_management, 4), 'is_negative': False,
'action': 'yuthon_report.action_management_area_report', 'action_context': {}, 'is_area': True},
{'label': '可租面积(㎡)', 'value': round(total_rent, 4), 'is_negative': False,
- 'action': 'yuthon_report.action_management_area_report', 'action_context': {}, 'is_area': True},
- {'label': '已租面积(㎡)', 'value': round(total_leased, 4), 'is_negative': False,
- 'action': 'yuthon_report.action_management_area_report', 'action_context': {}, 'is_area': True},
+ 'action': 'yuthon_report.action_rent_area_report', 'action_context': {}, 'is_area': True},
+ {'label': '物业已租面积(㎡)', 'value': round(total_property_leased, 4), 'is_negative': False,
+ 'action': 'yuthon_report.action_leased_area_report', 'action_context': {}, 'is_area': True},
+ {'label': '合同已租面积(㎡)', 'value': round(total_contract_leased, 4), 'is_negative': False,
+ 'action': 'yuthon_report.action_leased_area_report', 'action_context': {}, 'is_area': True},
{'label': '出租率', 'value': rental_rate, 'is_negative': False,
- 'action': 'yuthon_report.action_management_area_report', 'action_context': {}, 'is_percent': True},
+ 'action': 'yuthon_report.action_rental_rate_report', 'action_context': {}, 'is_percent': True},
{'label': '收费率', 'value': 0.00, 'is_negative': False,
'action': '', 'action_context': {}, 'is_percent': True},
]
return {
'cards': cards,
- 'row_groups': [['管理面积(㎡)', '可租面积(㎡)', '已租面积(㎡)', '出租率', '收费率']],
+ 'row_groups': [['管理面积(㎡)', '可租面积(㎡)', '物业已租面积(㎡)', '合同已租面积(㎡)', '出租率', '收费率']],
}
- except Exception:
+ except Exception as e:
+ _logger.error("[DASHBOARD_AREA_ERROR] %s", e, exc_info=True)
return {'cards': [], 'row_groups': []}
diff --git a/yuthon_report/views/leased_area_report.xml b/yuthon_report/views/leased_area_report.xml
new file mode 100644
index 000000000..ed6ab15c9
--- /dev/null
+++ b/yuthon_report/views/leased_area_report.xml
@@ -0,0 +1,39 @@
+
+
+
+
+ 已租面积报表
+ receivable_report_client
+
+ {
+ "sideBar": true,
+ "report_config": {
+ "model": "leased.area.report",
+ "method": "get_leased_area_report",
+ "dateMode": "single",
+ "columns": [
+ {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"},
+ {"headerName": "物业id", "field": "物业id", "flex": 0.8},
+ {"headerName": "物业名称", "field": "物业名称", "flex": 1.8},
+ {"headerName": "合同编号", "field": "合同编号", "flex": 1.5},
+ {"headerName": "承租方", "field": "承租方", "flex": 1.2},
+ {"headerName": "产权证号", "field": "产权证号", "flex": 1.2},
+ {"headerName": "产权地址", "field": "产权地址", "flex": 1.8},
+ {"headerName": "物业已租面积", "field": "物业已租面积", "flex": 1.2, "type": "number"},
+ {"headerName": "合同已租面积", "field": "合同已租面积", "flex": 1.2, "type": "number"},
+ {"headerName": "片区管理员", "field": "片区管理员", "flex": 1.2}
+ ]
+ }
+ }
+
+
+
+
+
+
diff --git a/yuthon_report/views/management_area_report.xml b/yuthon_report/views/management_area_report.xml
index 8f64e52a3..c6778603b 100644
--- a/yuthon_report/views/management_area_report.xml
+++ b/yuthon_report/views/management_area_report.xml
@@ -14,14 +14,13 @@
"columns": [
{"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"},
{"headerName": "物业id", "field": "物业id", "flex": 0.8},
- {"headerName": "产权证号", "field": "产权证号", "flex": 1.2},
{"headerName": "物业名称", "field": "物业名称", "flex": 1.8},
- {"headerName": "失效日期", "field": "失效日期", "flex": 1.2},
- {"headerName": "管理类型", "field": "管理类型", "flex": 1},
- {"headerName": "管理面积", "field": "管理面积", "flex": 1, "type": "number"},
- {"headerName": "可租面积", "field": "可租面积", "flex": 1, "type": "number"},
{"headerName": "合同编号", "field": "合同编号", "flex": 1.5},
- {"headerName": "总面积", "field": "总面积", "flex": 1, "type": "number"}
+ {"headerName": "承租方", "field": "承租方", "flex": 1.2},
+ {"headerName": "产权证号", "field": "产权证号", "flex": 1.2},
+ {"headerName": "产权地址", "field": "产权地址", "flex": 1.8},
+ {"headerName": "管理面积", "field": "管理面积", "flex": 1, "type": "number"},
+ {"headerName": "片区管理员", "field": "片区管理员", "flex": 1.2}
]
}
}
@@ -37,4 +36,3 @@
/>
-
diff --git a/yuthon_report/views/penalty_arrears_report.xml b/yuthon_report/views/penalty_arrears_report.xml
index 47712e270..21fc62233 100644
--- a/yuthon_report/views/penalty_arrears_report.xml
+++ b/yuthon_report/views/penalty_arrears_report.xml
@@ -10,29 +10,30 @@
"report_config": {
"model": "property.accounts.receivable",
"method": "get_penalty_arrears_report",
+ "dateMode": "end",
"columns": [
- {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"},
- {"headerName": "合同编号", "field": "合同编号", "flex": 1.2},
- {"headerName": "物业名称", "field": "物业名称", "flex": 2},
- {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4},
- {"headerName": "金额", "field": "金额", "flex": 1, "type": "number"},
- {"headerName": "应收金额", "field": "应收金额", "flex": 1, "type": "number"},
- {"headerName": "违约计算时间(天)", "field": "违约计算时间(天)", "flex": 2.5, "wrapText": true},
- {"headerName": "欠租金额", "field": "欠租金额", "flex": 1, "type": "number"},
- {"headerName": "每天违约金(元/天)", "field": "每天违约金(元/天)", "flex": 1.2, "type": "number"},
+ {"headerName": "租金ID", "field": "租金ID", "width": 60, "flex": 0},
+ {"headerName": "租金应收日", "field": "租金应收日", "flex": 1},
+ {"headerName": "租金收款日期", "field": "租金收款日期", "flex": 1},
+ {"headerName": "租金金额", "field": "租金金额", "flex": 1, "type": "number"},
+ {"headerName": "租金已收", "field": "租金已收", "flex": 1, "type": "number"},
+ {"headerName": "租金未收", "field": "租金未收", "flex": 1, "type": "number"},
+ {"headerName": "租金状态", "field": "租金状态", "flex": 0.8},
{"headerName": "违约金金额", "field": "违约金金额", "flex": 1, "type": "number"},
- {"headerName": "总已收金额", "field": "总已收金额", "flex": 1, "type": "number"},
- {"headerName": "总未收", "field": "总未收", "flex": 1, "type": "number"},
- {"headerName": "当月收款时间", "field": "当月收款时间", "flex": 1.2},
- {"headerName": "当月收款金额", "field": "当月收款金额", "flex": 1, "type": "number"},
- {"headerName": "所属公司", "field": "所属公司", "flex": 1},
- {"headerName": "产权证号", "field": "产权证号", "flex": 1.5},
- {"headerName": "管理员", "field": "管理员", "flex": 1}
+ {"headerName": "违约金已收", "field": "违约金已收", "flex": 1, "type": "number"},
+ {"headerName": "违约金未收", "field": "违约金未收", "flex": 1, "type": "number"},
+ {"headerName": "违约金状态", "field": "违约金状态", "flex": 0.8},
+ {"headerName": "违约金应收开始", "field": "违约金应收开始", "flex": 1},
+ {"headerName": "收费区间", "field": "收费区间", "flex": 1.5, "wrapText": true},
+ {"headerName": "备注", "field": "备注", "flex": 2, "wrapText": true},
+ {"headerName": "合同编号", "field": "合同编号", "flex": 1},
+ {"headerName": "物业名称", "field": "物业名称", "flex": 1.5},
+ {"headerName": "承租方名称", "field": "承租方名称", "flex": 1},
+ {"headerName": "所属公司", "field": "所属公司", "flex": 1}
]
}
}
-
-
+
\ No newline at end of file
diff --git a/yuthon_report/views/penalty_month_received_report.xml b/yuthon_report/views/penalty_month_received_report.xml
index 0c6ef4c7c..d9decc3e5 100644
--- a/yuthon_report/views/penalty_month_received_report.xml
+++ b/yuthon_report/views/penalty_month_received_report.xml
@@ -10,29 +10,33 @@
"report_config": {
"model": "property.accounts.receivable",
"method": "get_penalty_month_received_report",
+ "dateMode": "range",
"columns": [
- {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"},
- {"headerName": "合同编号", "field": "合同编号", "flex": 1.2},
- {"headerName": "物业名称", "field": "物业名称", "flex": 2},
- {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4},
- {"headerName": "金额", "field": "金额", "flex": 1, "type": "number"},
- {"headerName": "应收金额", "field": "应收金额", "flex": 1, "type": "number"},
- {"headerName": "违约计算时间(天)", "field": "违约计算时间(天)", "flex": 1.8},
- {"headerName": "欠租金额", "field": "欠租金额", "flex": 1, "type": "number"},
- {"headerName": "每天违约金(元/天)", "field": "每天违约金(元/天)", "flex": 1.2, "type": "number"},
+ {"headerName": "租金ID", "field": "租金ID", "width": 70, "flex": 0, "pinned": "left"},
+ {"headerName": "租金应收日", "field": "租金应收日", "flex": 1},
+ {"headerName": "租金收款日期", "field": "租金收款日期", "flex": 1},
+ {"headerName": "租金金额", "field": "租金金额", "flex": 1, "type": "number"},
+ {"headerName": "租金已收", "field": "租金已收", "flex": 1, "type": "number"},
+ {"headerName": "租金未收", "field": "租金未收", "flex": 1, "type": "number"},
+ {"headerName": "租金状态", "field": "租金状态", "flex": 0.8},
{"headerName": "违约金金额", "field": "违约金金额", "flex": 1, "type": "number"},
- {"headerName": "总已收金额", "field": "总已收金额", "flex": 1, "type": "number"},
- {"headerName": "总未收", "field": "总未收", "flex": 1, "type": "number"},
- {"headerName": "当月收款时间", "field": "当月收款时间", "flex": 1.2},
- {"headerName": "当月收款金额", "field": "当月收款金额", "flex": 1, "type": "number"},
- {"headerName": "所属公司", "field": "所属公司", "flex": 1},
- {"headerName": "产权证号", "field": "产权证号", "flex": 1.5},
- {"headerName": "管理员", "field": "管理员", "flex": 1}
+ {"headerName": "违约金已收", "field": "违约金已收", "flex": 1, "type": "number"},
+ {"headerName": "违约金未收", "field": "违约金未收", "flex": 1, "type": "number"},
+ {"headerName": "违约金状态", "field": "违约金状态", "flex": 0.8},
+ {"headerName": "违约金应收开始", "field": "违约金应收开始", "flex": 1},
+ {"headerName": "实收金额", "field": "实收金额", "flex": 1, "type": "number"},
+ {"headerName": "收款日期", "field": "收款日期", "flex": 1},
+ {"headerName": "收款备注", "field": "收款备注", "flex": 1.5, "wrapText": true},
+ {"headerName": "收费区间", "field": "收费区间", "flex": 1.5, "wrapText": true},
+ {"headerName": "备注", "field": "备注", "flex": 2, "wrapText": true},
+ {"headerName": "合同编号", "field": "合同编号", "flex": 1.2},
+ {"headerName": "物业名称", "field": "物业名称", "flex": 1.2},
+ {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.2},
+ {"headerName": "所属公司", "field": "所属公司", "flex": 1}
]
}
}
-
diff --git a/yuthon_report/views/penalty_year_cum_report.xml b/yuthon_report/views/penalty_year_cum_report.xml
index 12a7dddd6..cb968242c 100644
--- a/yuthon_report/views/penalty_year_cum_report.xml
+++ b/yuthon_report/views/penalty_year_cum_report.xml
@@ -10,20 +10,33 @@
"report_config": {
"model": "property.accounts.receivable",
"method": "get_penalty_year_cum_report",
+ "dateMode": "range",
"columns": [
- {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"},
+ {"headerName": "租金ID", "field": "租金ID", "width": 70, "flex": 0, "pinned": "left"},
+ {"headerName": "租金应收日", "field": "租金应收日", "flex": 1},
+ {"headerName": "租金收款日期", "field": "租金收款日期", "flex": 1},
+ {"headerName": "租金金额", "field": "租金金额", "flex": 1, "type": "number"},
+ {"headerName": "租金已收", "field": "租金已收", "flex": 1, "type": "number"},
+ {"headerName": "租金未收", "field": "租金未收", "flex": 1, "type": "number"},
+ {"headerName": "租金状态", "field": "租金状态", "flex": 0.8},
+ {"headerName": "违约金金额", "field": "违约金金额", "flex": 1, "type": "number"},
+ {"headerName": "违约金已收", "field": "违约金已收", "flex": 1, "type": "number"},
+ {"headerName": "违约金未收", "field": "违约金未收", "flex": 1, "type": "number"},
+ {"headerName": "违约金状态", "field": "违约金状态", "flex": 0.8},
+ {"headerName": "违约金应收开始", "field": "违约金应收开始", "flex": 1},
+ {"headerName": "实收金额", "field": "实收金额", "flex": 1, "type": "number"},
+ {"headerName": "收款日期", "field": "收款日期", "flex": 1},
+ {"headerName": "收款备注", "field": "收款备注", "flex": 1.5, "wrapText": true},
+ {"headerName": "收费区间", "field": "收费区间", "flex": 1.5, "wrapText": true},
+ {"headerName": "备注", "field": "备注", "flex": 2, "wrapText": true},
{"headerName": "合同编号", "field": "合同编号", "flex": 1.2},
- {"headerName": "物业名称", "field": "物业名称", "flex": 2.5},
- {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4},
- {"headerName": "金额", "field": "金额", "flex": 1, "type": "number"},
- {"headerName": "应收金额", "field": "应收金额", "flex": 1, "type": "number"},
- {"headerName": "所属公司", "field": "所属公司", "flex": 1},
- {"headerName": "管理员", "field": "管理员", "flex": 1}
+ {"headerName": "物业名称", "field": "物业名称", "flex": 1.2},
+ {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.2},
+ {"headerName": "所属公司", "field": "所属公司", "flex": 1}
]
}
}
-
diff --git a/yuthon_report/views/rent_area_report.xml b/yuthon_report/views/rent_area_report.xml
new file mode 100644
index 000000000..99c69f36d
--- /dev/null
+++ b/yuthon_report/views/rent_area_report.xml
@@ -0,0 +1,38 @@
+
+
+
+
+ 可租面积报表
+ receivable_report_client
+
+ {
+ "sideBar": true,
+ "report_config": {
+ "model": "rent.area.report",
+ "method": "get_rent_area_report",
+ "dateMode": "single",
+ "columns": [
+ {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"},
+ {"headerName": "物业id", "field": "物业id", "flex": 0.8},
+ {"headerName": "物业名称", "field": "物业名称", "flex": 1.8},
+ {"headerName": "合同编号", "field": "合同编号", "flex": 1.5},
+ {"headerName": "承租方", "field": "承租方", "flex": 1.2},
+ {"headerName": "产权证号", "field": "产权证号", "flex": 1.2},
+ {"headerName": "产权地址", "field": "产权地址", "flex": 1.8},
+ {"headerName": "可租面积", "field": "可租面积", "flex": 1, "type": "number"},
+ {"headerName": "片区管理员", "field": "片区管理员", "flex": 1.2}
+ ]
+ }
+ }
+
+
+
+
+
+
diff --git a/yuthon_report/views/rental_rate_report.xml b/yuthon_report/views/rental_rate_report.xml
new file mode 100644
index 000000000..6b80e4040
--- /dev/null
+++ b/yuthon_report/views/rental_rate_report.xml
@@ -0,0 +1,41 @@
+
+
+
+
+ 出租率报表
+ receivable_report_client
+
+ {
+ "sideBar": true,
+ "report_config": {
+ "model": "rental.rate.report",
+ "method": "get_rental_rate_report",
+ "dateMode": "single",
+ "columns": [
+ {"headerName": "序号", "field": "序号", "width": 45, "flex": 0, "pinned": "left"},
+ {"headerName": "物业id", "field": "物业id", "flex": 0.8},
+ {"headerName": "物业名称", "field": "物业名称", "flex": 1.8},
+ {"headerName": "合同编号", "field": "合同编号", "flex": 1.5},
+ {"headerName": "承租方", "field": "承租方", "flex": 1.2},
+ {"headerName": "产权证号", "field": "产权证号", "flex": 1.2},
+ {"headerName": "产权地址", "field": "产权地址", "flex": 1.8},
+ {"headerName": "可租面积", "field": "可租面积", "flex": 1, "type": "number"},
+ {"headerName": "物业已租面积", "field": "物业已租面积", "flex": 1.2, "type": "number"},
+ {"headerName": "合同已租面积", "field": "合同已租面积", "flex": 1.2, "type": "number"},
+ {"headerName": "出租率(%)", "field": "出租率", "flex": 1, "type": "number"},
+ {"headerName": "片区管理员", "field": "片区管理员", "flex": 1.2}
+ ]
+ }
+ }
+
+
+
+
+
+