diff --git a/yuthon_report/__manifest__.py b/yuthon_report/__manifest__.py index 2d5d79005..06ad0c92e 100644 --- a/yuthon_report/__manifest__.py +++ b/yuthon_report/__manifest__.py @@ -11,6 +11,8 @@ 'views/pre_arrears_report.xml', 'views/pre_advance_report.xml', 'views/penalty_arrears_report.xml', + 'views/penalty_month_received_report.xml', + 'views/penalty_year_cum_report.xml', 'views/menu.xml', 'views/ledger_report_views.xml', 'views/receivable_report_views.xml', diff --git a/yuthon_report/models/__init__.py b/yuthon_report/models/__init__.py index 363c42ba5..0753b36b8 100644 --- a/yuthon_report/models/__init__.py +++ b/yuthon_report/models/__init__.py @@ -35,4 +35,6 @@ from . import att_report from . import staff_position_report from . import pre_arrears_report from . import pre_advance_report -from . import penalty_arrears_report \ No newline at end of file +from . import penalty_arrears_report +from . import penalty_month_received_report +from . import penalty_year_cum_report \ No newline at end of file diff --git a/yuthon_report/models/penalty_month_received_report.py b/yuthon_report/models/penalty_month_received_report.py new file mode 100644 index 000000000..65bbcbf21 --- /dev/null +++ b/yuthon_report/models/penalty_month_received_report.py @@ -0,0 +1,122 @@ +import datetime + +from odoo import api, models + + +class PenaltyMonthReceivedReport(models.Model): + _inherit = 'property.accounts.receivable' + + @api.model + def get_penalty_month_received_report(self, params): + """ + 违约金本月实收报表: + - 筛选违约金记录(is_penalty=True 或 management_type_id 含"违约") + - 本月内有收款:payout_date 在本月第一天到最后一天之间 + - 每条记录单独一行,字段与违约金累计欠收一致 + """ + 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', '违约')]) + + domain = [ + ('payout_date', '>=', this_month_first), + ('payout_date', '<=', this_month_last), + ('is_invalid', '=', False), + ] + + # 违约金过滤 + if penalty_types: + domain += ['|', ('management_type_id', 'in', penalty_types.ids), ('is_penalty', '=', True)] + else: + domain.append(('is_penalty', '=', True)) + + keyword = params.get("keyword") + if keyword: + domain += [ + '|', '|', + ('contract_id.contract_code', 'ilike', keyword), + ('property_name', 'ilike', keyword), + ('lessee_id.name', 'ilike', keyword), + ] + + condition = params.get('condition') + partner_id = params.get('partner_id') + company_id = params.get('company_id') + + 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)) + + receivables = self.search(domain, order="contract_id asc, due_date asc") + + rows = [] + total_penalty = total_collected = total_uncollected = 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 + + 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 + + total_penalty += penalty_amount + total_collected += collected + total_uncollected += uncollected + + 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") + + rows.append({ + "序号": idx, + "合同编号": { + 'model': 'property.lease.contract', + 'id': rec.contract_id.id, + 'display_name': rec.contract_id.contract_code, + } if rec.contract_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), + "违约计算时间(天)": 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 "", + }) + + stats = [ + {'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} diff --git a/yuthon_report/models/penalty_year_cum_report.py b/yuthon_report/models/penalty_year_cum_report.py new file mode 100644 index 000000000..2df9d40e6 --- /dev/null +++ b/yuthon_report/models/penalty_year_cum_report.py @@ -0,0 +1,104 @@ +import datetime + +from odoo import api, models + + +class PenaltyYearCumReport(models.Model): + _inherit = 'property.accounts.receivable' + + @api.model + def get_penalty_year_cum_report(self, params): + """ + 违约金本年累计报表: + - 筛选违约金记录(is_penalty=True 或 management_type_id 含"违约") + - 本年内有收款:payout_date 在本年第一天到最后一天之间 + - 按合同分组,金额 = sum(penalty) + - 字段:序号/合同编号/物业名称/承租方名称/金额/所属公司/管理员 + """ + 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', '违约')]) + + domain = [ + ('payout_date', '>=', year_first), + ('payout_date', '<=', year_last), + ('is_invalid', '=', False), + ] + + # 违约金过滤 + if penalty_types: + domain += ['|', ('management_type_id', 'in', penalty_types.ids), ('is_penalty', '=', True)] + else: + domain.append(('is_penalty', '=', True)) + + keyword = params.get("keyword") + if keyword: + domain += [ + '|', '|', + ('contract_id.contract_code', 'ilike', keyword), + ('property_name', 'ilike', keyword), + ('lessee_id.name', 'ilike', keyword), + ] + + condition = params.get('condition') + partner_id = params.get('partner_id') + company_id = params.get('company_id') + + 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)) + + receivables = self.search(domain, order="contract_id asc, due_date asc") + + # 按合同分组,金额 = 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, + } + contract_map[cid]['total_penalty'] += rec.penalty 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 + + rows.append({ + "序号": idx, + "合同编号": { + 'model': 'property.lease.contract', + 'id': contract.id, + 'display_name': contract.contract_code, + } if contract 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, + "所属公司": 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 "", + }) + + stats = [ + {'name': '合同总数', 'amount': len(rows)}, + {'name': '违约金总额', 'amount': round(total_amount, 2)}, + ] + + return {"rows": rows, "stats": stats} diff --git a/yuthon_report/views/menu.xml b/yuthon_report/views/menu.xml index 8d1956578..894760da4 100644 --- a/yuthon_report/views/menu.xml +++ b/yuthon_report/views/menu.xml @@ -77,12 +77,12 @@ sequence="2" name="本月实收" parent="yuthon_property_penalty_menu" - action="action_cumulative_arrears_report"/> + action="action_penalty_month_received_report"/> + action="action_penalty_year_cum_report"/> + + + + 违约金本月实收 + receivable_report_client + + { + "sideBar": true, + "report_config": { + "model": "property.accounts.receivable", + "method": "get_penalty_month_received_report", + "columns": [ + {"headerName": "序号", "field": "序号", "flex": 0.5}, + {"headerName": "合同编号", "field": "合同编号", "flex": 1.2}, + {"headerName": "物业名称", "field": "物业名称", "flex": 2}, + {"headerName": "承租方名称", "field": "承租方名称", "flex": 1.4}, + {"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": "违约金金额", "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} + ] + } + } + + + + + + diff --git a/yuthon_report/views/penalty_year_cum_report.xml b/yuthon_report/views/penalty_year_cum_report.xml new file mode 100644 index 000000000..2a09201bc --- /dev/null +++ b/yuthon_report/views/penalty_year_cum_report.xml @@ -0,0 +1,35 @@ + + + + + 违约金本年累计 + receivable_report_client + + { + "sideBar": true, + "report_config": { + "model": "property.accounts.receivable", + "method": "get_penalty_year_cum_report", + "columns": [ + {"headerName": "序号", "field": "序号", "flex": 0.5}, + {"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}, + {"headerName": "管理员", "field": "管理员", "flex": 1} + ] + } + } + + + + + +