Files
2026-09-28 18:52:35 +08:00

1819 lines
84 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
from datetime import datetime, timedelta, date
from odoo.tools.float_utils import float_round
from numpy.ma.core import default_filler
from odoo import models, fields, api, exceptions, _
from odoo.exceptions import UserError, ValidationError
from dateutil.relativedelta import relativedelta
import calendar
import re
class PropertyMeterWater(models.Model):
_inherit = 'property.meter.water'
contract_id = fields.Many2one('property.lease.contract', string="合同", compute='compute_contract_id')
@api.depends('yuthon_property_ids')
def compute_contract_id(self):
for record in self:
if record.yuthon_property_ids:
lease_contract = self.env['property.lease.contract.line'].search(
[('address_id', '=', record.yuthon_property_ids[0].id)], limit=1)
if lease_contract:
record.contract_id = lease_contract.lease_id.id
else:
record.contract_id = False
else:
record.contract_id = False
class PropertyMeterArchives(models.Model):
_inherit = 'property.meter.archives'
contract_id = fields.Many2one('property.lease.contract', string="合同", compute='compute_contract_id')
@api.depends('yuthon_property_ids')
def compute_contract_id(self):
for record in self:
if record.yuthon_property_ids:
lease_contract = self.env['property.lease.contract.line'].search(
[('address_id', '=', record.yuthon_property_ids[0].id)], limit=1)
if lease_contract:
record.contract_id = lease_contract.lease_id.id
else:
record.contract_id = False
else:
record.contract_id = False
class MeterArchivesLine(models.Model):
_inherit = 'meter.archives.line'
contract_id = fields.Many2one('property.lease.contract', string="合同")
lessee_id = fields.Many2one(related="contract_id.lessee_id", string="承租方", store=True)
contract_name = fields.Char(string='合同编号', store=True, index=True, compute="_compute_name")
lessee_name = fields.Char(string='承租方名称', store=True, index=True, compute="_compute_name")
@api.depends('yuthon_property_ids', 'archives_id', 'contract_id', 'lessee_id')
def _compute_name(self):
for name in self:
if name.contract_id:
name.contract_name = name.contract_id.name
else:
name.contract_name = False
if name.lessee_id:
name.lessee_name = name.lessee_id.name
else:
name.lessee_name = False
def update_contract(self):
archives_ids = self.env["meter.archives.line"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for record in archives_ids:
record.lessee_id = record.contract_id.lessee_id.id
class MeterWaterLine(models.Model):
_inherit = 'meter.water.line'
contract_id = fields.Many2one('property.lease.contract', string="合同")
lessee_id = fields.Many2one(related="contract_id.lessee_id", string="承租方")
contract_name = fields.Char(string='合同编号', store=True, index=True, compute="_compute_name")
lessee_name = fields.Char(string='承租方名称', store=True, index=True, compute="_compute_name")
@api.depends('yuthon_property_ids', 'water_id', 'contract_id', 'lessee_id')
def _compute_name(self):
for name in self:
if name.contract_id:
name.contract_name = name.contract_id.name
if name.lessee_id:
name.lessee_name = name.lessee_id.name
def update_contract(self):
water_ids = self.env["meter.water.line"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for record in water_ids:
if record.yuthon_property_ids:
contract_line = self.env['property.lease.contract.line'].search([
('address_id', '=', record.yuthon_property_ids[0].id),
('lease_id.actual_start_time', '<=',record.meter_date),
('lease_id.actual_end_time', '>=',record.meter_date)
], limit=1)
if contract_line:
record.contract_id = contract_line.lease_id.id
class YuthonProperty(models.Model):
_inherit = 'yuthon.property'
property_contract_ids = fields.One2many('property.contract.line', 'yuthon_property_id', string="关联物业合同明细行")
@api.depends('property_contract_ids')
def _compute_lessee_id(self):
today = date.today()
for record in self:
contract_lines = record.property_contract_ids
valid_lines = contract_lines.filtered(
lambda line:
line.start_time and
line.start_time <= today and
(not line.end_date or line.end_date >= today)
)
record.lessee_id = valid_lines[0].lessee_id if valid_lines else False
def get_yuthon_property_time(self):
unique_property_ids = set()
for record in self.search([('property_state', '=', 'to'), ('property_contract_ids', '=', False)]):
contracts = self.env['property.evaluation.plan'].search([])
for contract in contracts:
if contract.property_id and contract.property_id.id not in unique_property_ids:
unique_property_ids.add(contract.property_id.id)
if record.id not in unique_property_ids:
record.put_assessment_approval()
else:
continue
def update_lessee_id(self):
property_ids = self.env["yuthon.property"].browse(
self._context.get('active_ids', [self._context.get('active_id')])
)
current_date = date.today()
for rec in property_ids:
valid_contracts = rec.property_contract_ids.filtered(lambda c:
isinstance(c.start_time,
date) and c.start_time <= current_date and
(not c.end_date or (isinstance(c.end_date,
date) and c.end_date > current_date))
)
if valid_contracts:
latest_contract = valid_contracts.sorted(key=lambda c: c.start_time, reverse=True)[0]
rec.lessee_id = latest_contract.lessee_id
else:
rec.lessee_id = False
@api.onchange('property_contract_ids')
def _onchange_property_contract_ids(self):
if self.property_contract_ids:
self.lessee_id = self.property_contract_ids[-1].lessee_id
# 银行流水
class PropertyBankStatements(models.Model):
_inherit = 'property.bank.statements'
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")
contract_ids = fields.Many2many(
'property.lease.contract',
'property_bank_stmt_contract_rel',
'stmt_id',
'contract_id',
string='合同',
compute = '_compute_property_employee_id',
store=True,
)
contract_ids1 = fields.Many2many(
'property.lease.contract',
'property_bank_stmt_contract1_rel',
'stmt_id',
'contract_id',
string='已对合同'
)
property_name = fields.Text(string="物业名称", compute="compute_property_name", store=True)
accounts_id = fields.Many2one('property.accounts.approval', string="关联生成应收审批单")
lessee_ids = fields.Many2many('property.tenant.information', string='承租方', compute="_compute_property_employee_id", store=True)
admin_id = fields.Many2one('res.users', string="片区管理员", compute="_compute_property_employee_id", store=True)
def write_off_details(self):
self.ensure_one()
modify_lines = self.env['receivable.modify.line'].search([
('bank_id', '=', self.id)
])
receivable_ids = modify_lines.mapped('receivable_id').filtered(lambda r: r.id)
return {
'type': 'ir.actions.act_window',
'res_model': 'property.accounts.receivable',
'view_mode': 'tree',
'view_id': self.env.ref('yuthon_property.view_property_accounts_receivable_tree').id,
'domain': [('id', 'in', receivable_ids.ids)],
}
@api.depends('name')
def _compute_property_employee_id(self):
for record in self:
lessee_ids = []
if record.name:
bank_lines = self.env['property.bank.line'].search([('bank', '=', record.name)])
for line in bank_lines:
tenant = line.tenant_id
# 租户存在 且 承租方明细行不为空
if tenant and tenant.property_contract_ids:
lessee_ids.append(tenant.id)
# 去重
lessee_ids = list(set(lessee_ids))
record.lessee_ids = [(6, 0, lessee_ids)]
# 2. 重构核心逻辑:按日期筛选出租方变更行,匹配所属单元
if record.lessee_ids:
contract_set = self.env['property.lease.contract']
# 获取当前记录的日期(用于判断出租方变更行的生效期)
target_date = record.start_time.date() if record.start_time else fields.Date.today()
# ====================== 提取目标年月 ======================
target_year = target_date.year
target_month = target_date.month
for lessee in record.lessee_ids:
# 从承租方的租户行提取合同
tenant_lines = lessee.property_contract_ids.filtered(
lambda line: line.lease_contract_id is not False
)
for tenant_line in tenant_lines:
contract = tenant_line.lease_contract_id
if not contract:
continue
# 按【年月】判断合同是否有效
is_contract_valid = False
if contract.actual_start_time:
start_year = contract.actual_start_time.year
start_month = contract.actual_start_time.month
# 合同开始年月 <= 目标年月
if (start_year < target_year) or (
start_year == target_year and start_month <= target_month):
# 合同结束时间判断:未设置 或 结束年月 >= 目标年月
if not contract.actual_end_time:
is_contract_valid = True
else:
end_year = contract.actual_end_time.year
end_month = contract.actual_end_time.month
if (end_year > target_year) or (
end_year == target_year and end_month >= target_month):
is_contract_valid = True
if not is_contract_valid:
continue
# 1. 筛选合同的出租方变更行:生效日期≤目标日期,失效日期≥目标日期/未设置
valid_lessor_lines = contract.lessor_line.filtered(
lambda l:
l.start_date <= target_date and
(not l.end_date or l.end_date >= target_date)
)
# 2. 确定最终的出租方referred值
if valid_lessor_lines:
actual_lessor_referred = valid_lessor_lines[0].lessor_id.referred
else:
actual_lessor_referred = contract.lessor_id.referred if contract.lessor_id else False
if actual_lessor_referred == record.your_own_unit:
if contract.contact_state != 'settle':
contract_set |= contract
record.contract_ids = contract_set
record.admin_id = record.contract_ids[0].admin_id if record.contract_ids else False
else:
record.admin_id = False
record.contract_ids = [(6, 0, [])]
def update_property_employee_id(self):
property_ids = self.env["property.bank.statements"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in property_ids:
rec._compute_property_employee_id()
@api.depends('contract_ids')
def compute_property_name(self):
for rec in self:
rec.property_name = ""
if rec.contract_ids:
for contract in rec.contract_ids:
if contract.property_name:
rec.property_name += contract.property_name + "\n"
def batch_revocation(self):
bank_ids = self.env["property.bank.statements"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in bank_ids:
rec.revocation()
def test_one(self):
bank_ids = self.env["property.bank.statements"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in bank_ids:
rec.start_time = rec.start_time - timedelta(hours=8)
def contract_context(self):
# 总感觉关联有问题,流水应该是取物业关联的当期合同,有个时间情况没考虑到
bank_ids = self.env["property.bank.statements"].browse(self._context.get('active_ids', self._context.get('active_id')))
for property in bank_ids.search([('admin_id', '!=', False)]):
for pro in property.property_ids:
# 内部逻辑的关联还有很大问题
if len(pro.property_contract_ids) > 0:
if len(pro.property_contract_ids.lease_contract_id) > 0:
property.write({'contract_ids': [(4, pro.property_contract_ids[-1].lease_contract_id.id)]})
property.write({'lessee_ids': [(4, pro.property_contract_ids[-1].lessee_id.id)]})
def revocation(self):
self.ensure_one()
for modify in self.receivable_modify_ids:
if modify.is_write:
new_delivered = modify.receivable_id.delivered1 - modify.amount
modify.receivable_id.delivered1 = max(new_delivered, 0)
modify.receivable_id.is_reconciliation = False
if modify.receivable_id.delivered1 > 0:
modify.receivable_id.deposit_state = 'no_paid'
else:
modify.receivable_id.deposit_state = 'unpaid'
modify.receivable_id.reconciliation_time = False
modify.receivable_id.payout_date = False
if not modify.is_penalty:
modify.receivable_id.remark = None
else:
if modify.is_penalty:
modify.receivable_id.unlink()
if self.receivable_modify_ids:
self.receivable_modify_ids.unlink()
if self.bank_state != 'registered':
self.yes_amount = self.original_amount - self.change_amount
else:
self.yes_amount = 0
self.bank_state = 'not_reconciled'
self.registered = 0
self.reconciliation_time = None
self._filter_contracts_based_on_receivables()
def _filter_contracts_based_on_receivables(self):
"""还原合同"""
for stmt in self:
if not stmt.contract_ids1:
continue
new_contract_ids = stmt.contract_ids.ids + stmt.contract_ids1.ids
stmt.contract_ids = [(6, 0, new_contract_ids)]
def binding_information(self):
return {
'name': '信息绑定弹窗',
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'binding.information.wizard',
'target': 'new',
'context': {
'default_lessee_ids': [(6, 0, self.lessee_ids.ids)] if self.lessee_ids else False,
'default_bank_id': self.id,
'default_your_own_unit': self.your_own_unit,
}
}
def unbind(self):
statements_ids = self.env["property.bank.statements"].browse(
self._context.get('active_ids', self._context.get('active_id'))
)
for statement in statements_ids:
if statement.name:
bank_account = self.env['property.bank.line'].search([
('bank', '=', statement.name)
])
if bank_account.tenant_id:
# 获取需要删除的合同明细行
lines_to_delete = self.env['property.tenant.line'].search([
('tenant_id', '=', bank_account.tenant_id.id),
('lease_contract_id', 'in', statement.contract_ids.ids)
])
# 删除合同明细行
if lines_to_delete:
lines_to_delete.unlink()
statement.contract_ids = [(5,)]
def _get_reconciliation_vals(self, record, contract_ids, is_equity=False):
if record.name:
collection_type_map = {
'rent': '租金',
'deposit': '押金',
'management_fee': '管理费',
'lightbox': '灯箱',
'guanzijian': '管子间',
'water_fee': '水费',
'electricity_fee': '电费',
'rename_fee': '更名费',
'service': '有偿服务收入',
'fee': '人工费/日光灯款/代垫用料费',
}
used_type_names = set()
total_rent_month = 0
base_domain = [
('contract_id', 'in', contract_ids.ids),
('is_invalid', '=', False),
('is_reconciliation', '=', False),
]
if not is_equity:
base_domain.append(('contract_id.lessor_id.referred', '=', self.your_own_unit))
def _search_receivable(management_type_name):
domain = base_domain + [('management_type_id.name', '=', management_type_name)]
return self.env['property.accounts.receivable'].search(domain)
service_ids = _search_receivable('有偿服务收入')
rent_ids = _search_receivable('租金')
rename_fee_ids = _search_receivable('更名费')
deposit_ids = _search_receivable('押金')
management_ids = _search_receivable('管理费')
lightbox_ids = _search_receivable('灯箱')
guanzi_ids = _search_receivable('管子间')
water_ids = _search_receivable('水费')
electricity_ids = _search_receivable('电费')
fee_ids = _search_receivable('人工费/日光灯款/代垫用料费')
def create_reconciliation_vals(ids, collection_type_val, sequence_val):
"""
:param ids:费用记录集
:param collection_type_val: 费用类型(如 'rent', 'deposit')
:return: 格式化后的费用行列表
"""
nonlocal total_rent_month
vals_list = []
current_sequence = sequence_val
type_name = collection_type_map.get(collection_type_val, '')
# # 特殊处理:押金类型需要去重相同合同的记录
# if collection_type_val == 'deposit':
# # 使用字典来跟踪已处理的合同ID
# processed_contracts = set()
# for sub_record in ids:
# contract_id = sub_record.contract_id.id if sub_record.contract_id else False
# original_amount = sub_record.original_amount if sub_record.original_amount else 0
# submitted = sub_record.submitted if sub_record.submitted else 0
# # 跳过已处理的合同
# if contract_id and contract_id in processed_contracts:
# continue
# # 获取合同的实际结束日期作为到期日
# due_date = False
# if contract_id:
# due_date = sub_record.contract_id.actual_start_time
# # 构建对账行数据
# vals = {
# 'contract_id': contract_id,
# 'rent_month': original_amount,
# 'rent': submitted,
# 'collection_type': collection_type_val,
# 'sequence': current_sequence,
# 'due_date': due_date, # 押金类型使用合同实际开始日期
# }
# total_rent_month += original_amount # 累加总金额
# vals_list.append((0, 0, vals))
# # 标记此合同已处理
# if contract_id:
# processed_contracts.add(contract_id)
#
# current_sequence += 10
# else:
# # 非押金类型:正常处理每条记录
for sub_record in ids:
contract_id = sub_record.contract_id.id if sub_record.contract_id else False
original_amount = sub_record.original_amount if sub_record.original_amount else 0
submitted = sub_record.submitted if sub_record.submitted else 0
due_date = sub_record.due_date if sub_record.due_date else False
vals = {
'contract_id': contract_id,
'rent_month': original_amount,
'rent': submitted,
'collection_type': collection_type_val,
'sequence': current_sequence,
'due_date': due_date,
}
total_rent_month += original_amount # 累加总金额
vals_list.append((0, 0, vals))
current_sequence += 10
if vals_list and type_name:
used_type_names.add(type_name)
return vals_list, current_sequence
all_vals = []
service_vals, next_seq = create_reconciliation_vals(service_ids, 'service', 1)
rent_vals, next_seq = create_reconciliation_vals(rent_ids, 'rent', 1)
deposit_vals, next_seq = create_reconciliation_vals(deposit_ids, 'deposit', 100)
management_vals, next_seq = create_reconciliation_vals(management_ids, 'management_fee', 200)
rename_vals, next_seq = create_reconciliation_vals(rename_fee_ids, 'rename_fee', 210)
lightbox_vals, next_seq = create_reconciliation_vals(lightbox_ids, 'lightbox', 220)
guanzi_vals, next_seq = create_reconciliation_vals(guanzi_ids, 'guanzijian', 230)
water_vals, next_seq = create_reconciliation_vals(water_ids, 'water_fee', 300)
electricity_vals, next_seq = create_reconciliation_vals(electricity_ids, 'electricity_fee', 400)
fee_vals, next_seq = create_reconciliation_vals(fee_ids, 'fee', 500)
all_vals.extend(rent_vals)
all_vals.extend(deposit_vals)
all_vals.extend(lightbox_vals)
all_vals.extend(guanzi_vals)
all_vals.extend(service_vals)
all_vals.extend(management_vals)
all_vals.extend(water_vals)
all_vals.extend(electricity_vals)
all_vals.extend(fee_vals)
all_vals.extend(rename_vals)
# 按 due_date 正序排列,最早日期在最上面
all_vals.sort(key=lambda x: (x[2].get('due_date') or date.max, x[2].get('contract_id') or 0))
# 重新设置 sequence,确保按正序从 10 开始递增
for idx, val in enumerate(all_vals, start=1):
val[2]['sequence'] = idx * 10
return all_vals, total_rent_month,
return [], 0
def _get_context_and_action(self, record, all_vals, total_rent_month):
"""
:param record: 银行收款记录
:param all_vals: 应收费用列表
:param total_rent_month: 应收总金额
:return:
"""
start_time = self.start_time + relativedelta(hours=8)
warning_msg = self._check_unreconciled_statements(start_time)
context = {
"default_actual_date": fields.Date.to_date(start_time),
"default_bank_id": self.id,
"default_bank_branch": self.bank_branch,
"default_account": record.name,
"default_contract_ids": [(6, 0, self.contract_ids.ids)],
"default_actual_total": record.original_amount,
"default_reconciliation_line_ids": all_vals, # 租金,应收日期排序、管理费、电、水、押金
'default_amount_reconciled': self.no_amount,
"default_receivable_total": total_rent_month,
"default_warning_msg":warning_msg,
"dialog_size": "extra-large",
"default_order": "due_date desc",
}
return {
"type": "ir.actions.act_window",
"name": "对账弹窗",
"res_model": "property.reconciliation.wizard",
"view_mode": "form",
"target": "new",
"dialog_size": "extra-large",
"context": context,
}
def _check_unreconciled_statements(self, current_transaction_time):
"""
检查所有承租方对应的银行账号在property.bank.statements中是否有未对账流水
:param current_transaction_time: 当前交易时间(带时区处理后)
"""
if not self.lessee_ids:
return
bank_accounts = self.env['property.bank.line'].search([
('tenant_id', 'in', self.lessee_ids.ids)
]).mapped('bank')
bank_accounts = [acc for acc in bank_accounts if acc] # 过滤空值
if not bank_accounts:
return
unreconciled = {}
for account in bank_accounts:
# 查询条件:同账号 + 交易时间早于当前 + 未对账
statements = self.env['property.bank.statements'].search([
('name', '=', account),
('start_time', '<', current_transaction_time - timedelta(hours=8)),
('your_own_unit', '=', self.your_own_unit),
('lending_direction', '=', self.lending_direction),
('bank_state', '=', 'not_reconciled'),
('id', '!=', self.id),
])
if statements:
unreconciled[account] = len(statements)
if unreconciled:
warning_msg = _("⚠️ 检测到未对账的历史流水,请优先处理(可忽略)")
else:
warning_msg = False
return warning_msg
def button_reconciliation(self):
for record in self:
all_vals, total_rent_month = self._get_reconciliation_vals(record, record.contract_ids)
return self._get_context_and_action(record, all_vals, total_rent_month)
def put_button(self):
form_id = self.env.ref('yuthon_property_approval.view_property_accounts_approval_form').id
bank_list = []
bank_list.append((0, 0, {
'start_time': self.start_time,
'company_id': self.company_id.id if self.company_id else False,
'name': self.name,
'bank_name': self.bank_name,
'name_account': self.name_account,
'purpose': self.purpose,
'journal_date': self.journal_date,
'bank_branch': self.bank_branch,
'extract': self.extract,
'account_code': self.account_code,
'is_put': self.is_put,
'remake': self.remake,
'original_amount': self.original_amount,
'contract_account': self.contract_account,
'contract_penalty': self.contract_penalty,
'property_ids': [(6, 0, self.property_ids.ids)] if self.property_ids else [],
'contract_ids': [(6, 0, self.contract_ids.ids)] if self.contract_ids else [],
'bank_number': self.bank_number,
'message': self.message,
'number': self.number,
'voucher_type': self.voucher_type
}))
return {
"type": "ir.actions.act_window",
"name": "应收账单生成审批单",
"res_model": "property.accounts.approval",
"views": [[form_id, "form"]],
"context": {
"default_bank_ids": bank_list,}}
class PropertyBankLine(models.Model):
_inherit = 'property.bank.line'
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")
# 应收账单
class PropertyAccountsReceivable(models.Model):
_inherit = 'property.accounts.receivable'
_rec_name = 'contract_id'
property_ids = fields.Many2many('yuthon.property', string='物业', compute='_compute_property_ids', store=True)
contract_accounts_id = fields.Many2one('contract.accounts.receivable', string="合同")
property_name = fields.Text(string='物业', compute="_compute_property_name")
is_penalty = fields.Boolean(string='是否违约金', default=True, compute = '_compute_is_penalty', store=True)
management_area = fields.Float(related='contract_id.total_area', string='面积')
title_deed = fields.Char(string="产权证号", compute='_compute_title_deed')
lessee_id = fields.Many2one('property.tenant.information', string='承租方', store=True, compute='_compute_lessee_id')
company_id = fields.Many2one('yuthon.property.company', string="出租方公司", store=True, compute='_compute_lessee_id')
# company_id1 = fields.Many2one(related='contract_id.admin_id.company_id', string="管理员公司", store=True)
company_id2 = fields.Many2one("res.company", string="公司", compute='_compute_company_id2', store=True)
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")
rent_id = fields.Char(string='租金id') # 没效果
source_receivable_id = fields.Many2one('property.accounts.receivable', string='关联id', index=True, copy=False)
contract_sort = fields.Selection([('property', '物业'), ('guanggao', '广告位'),('parking', '停车场'), ('guanzi', '管子间'), ('room', '机房'), ('base_station','基站'), ('other', '其他')],
related='contract_id.contract_sort', string="资产类型", index=True)
admin_id = fields.Many2one(related='contract_id.admin_id', string="片区管理员", store=True)
tenure_type = fields.Selection([('self', '自有'), ('agency', '代管')], string='权属类型', compute="compute_tenure_type", store=True)
tenure_type_label = fields.Char(string='权属类型(文本)', compute='_compute_tenure_type_label', store=True)
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')
# 报表新增字段
rent_owed = fields.Float(string="欠租金额")
per_day = fields.Float(string="每天违约金(元/天)")
last_month_table = fields.Float(string="上月表数")
this_month_table = fields.Float(string="本月表数")
ratio = fields.Float(string="倍率")
billable_quantity = fields.Float(string="计费数量")
pre_collection = fields.Float(string="预收金额")
actual_coping = fields.Float(string="实际应付", compute='_compute_actual_coping', store=True)
charging_range = fields.Text(string="收费区间")
precinct_id = fields.Many2one('yuthon.precinct.message', string="所属片区", compute='_compute_precinct_id', store=True)
# 核对人:应收账单创建时自动记录当前登录人
verifier_id = fields.Many2one('res.users', string="核对人", default=lambda self: self.env.user)
@api.model_create_multi
def create(self, vals_list):
"""创建应收账单时,核对人自动填当前登录人(未显式指定时)"""
for vals in vals_list:
if not vals.get('verifier_id'):
vals['verifier_id'] = self.env.user.id
return super().create(vals_list)
@api.depends('contract_id', 'contract_id.lease_ids', 'contract_id.lease_ids.address_id',
'contract_id.lease_ids.address_id.manage_information_ids', 'due_date')
def _compute_precinct_id(self):
"""根据合同第一条物业明细的管理信息,按应收开始日期匹配对应管理明细行,取其所属片区"""
for rec in self:
rec.precinct_id = False
if not rec.contract_id or not rec.contract_id.lease_ids:
continue
address = rec.contract_id.lease_ids[0].address_id
if not address or not address.manage_information_ids:
continue
due_date = rec.due_date
if not due_date:
continue
matched = address.manage_information_ids.filtered(
lambda m: m.start_time and m.start_time <= due_date and (not m.end_date or m.end_date >= due_date)
)
if matched:
rec.precinct_id = matched[0].precinct_id
else:
past = address.manage_information_ids.filtered(lambda m: m.start_time and m.start_time <= due_date)
if past:
rec.precinct_id = past.sorted('start_time', reverse=True)[0].precinct_id
def action_precinct_id(self):
record_ids = self.env["property.accounts.receivable"].browse(
self._context.get("active_ids", self._context.get("active_id"))
)
for rec in record_ids:
rec._compute_precinct_id()
def action_fix(self):
record_ids = self.env["property.accounts.receivable"].browse(
self._context.get("active_ids", self._context.get("active_id"))
)
for record in record_ids:
if record.management_type_id.name == '违约金' and record.due_date:
if record.is_reconciliation:
due_date = record.payout_date
else:
due_date = record.due_date
# 查询同合同、租金类型、payout_date匹配的所有租金记录
rent_ids = self.env['property.accounts.receivable'].search([
('contract_id', '=', record.contract_id.id),
('management_type_id.name', '=', '租金'),
('payout_date', '=', due_date),
])
if not rent_ids:
continue
valid_rents = rent_ids.filtered(lambda r:
r.due_date and r.payout_date >= (
r.due_date + relativedelta(months=1)).replace(day=1)
)
if valid_rents:
record.source_receivable_id = valid_rents[0].id
def action_receivable(self):
record_ids = self.env["property.accounts.receivable"].browse(
self._context.get("active_ids", self._context.get("active_id"))
)
for record in record_ids:
record.discount_amount = record.submitted
record.discount_date = datetime(2026, 1, 1)
def action_clear_legacy_unpaid(self):
"""一次性历史欠费清理(应收日期 < 2026-01-01 且 未对账,不限收款状态):
1) 应收类型 = 租金 → 关闭「生成违约金」(is_liquidated_damages = False)
2) 应收类型 = 违约金 → 作废 (is_invalid = True,作废日期 = 当天)
作废后汇总视图 property.accounts.receivable.summary 与「未收明细」跳转都会自动排除。
"""
cutoff = date(2026, 1, 1)
today = fields.Date.context_today(self)
base_domain = [
('due_date', '<', cutoff),
('is_reconciliation', '=', False),
]
# 1) 租金:关闭生成违约金(只处理当前还开着的,避免无谓写入)
rent_records = self.search(base_domain + [
('management_type_id.name', '=', '租金'),
('is_liquidated_damages', '=', True),
])
rent_records.write({'is_liquidated_damages': False})
# 2) 违约金:作废 + 填当天作废日期(已作废的不动,保留原作废日期)
penalty_records = self.search(base_domain + [
('management_type_id.name', '=', '违约金'),
('is_invalid', '=', False),
])
penalty_records.write({'is_invalid': True, 'void_date': today})
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'type': 'success',
'title': '清理完成',
'message': '租金关闭「生成违约金」%s 条;违约金作废 %s 条。'
% (len(rent_records), len(penalty_records)),
'sticky': False,
},
}
def verification_wizard(self):
receivable_lines = self.env['property.accounts.receivable'].search([
('contract_id', '=', self.contract_id.id),
('is_reconciliation', '=', False),
])
receivable_line_vals = []
for line in receivable_lines:
receivable_line_vals.append((0, 0, {
'due_date': line.due_date,
'management_type_id': line.management_type_id.id,
'amount': line.submitted,
'payment_type': 'pay',
'receivable_id': line.id,
'remake': line.remark,
}))
bills_lines = self.env['property.bills.list'].search([
('contract_id', '=', self.contract_id.id),
('actual_date', '!=', False),
])
bills_line_vals = []
for line in bills_lines:
bills_line_vals.append((0, 0, {
'due_date': line.due_date,
'management_type_id': line.management_type_id.id,
'amount': line.amount,
'bills_id': line.id,
'payment_type': 'spending',
'remake': line.remake
}))
all_line_vals = receivable_line_vals + bills_line_vals
return {
"type": "ir.actions.act_window",
"name": "押金核销弹窗",
"res_model": "property.deposit.wizard",
"view_mode": "form",
"target": "new",
"context": {
"default_receivable_id": self.id,
"default_contract_id": self.contract_id.id,
"default_lessee_id": self.lessee_id.id,
"default_actual_date": self.due_date,
"default_deposit": self.original_amount,
'default_reconciliation_line_ids': all_line_vals
}
}
def action_batch_fix_deposit(self):
"""批量修复2026年3月之前未收的押金数据,标记为已收并同步到修改明细"""
records = self.env['property.accounts.receivable'].search([
('management_type_id.name', '=', '押金'),
('due_date', '<', '2026-03-01'),
('deposit_state', '=', 'unpaid'),
])
if not records:
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '提示',
'message': '没有符合条件的数据需要修复。',
'type': 'info',
'sticky': False,
}
}
modify_line_vals = []
for rec in records:
delivered = rec.original_amount - rec.discount_amount
rec.write({
'deposit_state': 'paid',
'payout_date': rec.due_date,
'delivered1': delivered,
'is_reconciliation': True,
})
modify_line_vals.append({
'receivable_id': rec.id,
'reconciliation_date': rec.due_date,
'amount': delivered,
'is_batch_fix': True,
'remake1': '批量修复押金旧数据',
'bank_state': 'reconciled',
})
if modify_line_vals:
self.env['receivable.modify.line'].create(modify_line_vals)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '批量修复完成',
'message': f'成功修复 {len(records)} 条押金数据,并同步到修改明细。',
'type': 'success',
'sticky': False,
}
}
def update_compute_lessee_id(self):
rent_ids = self.env["property.accounts.receivable"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rent in rent_ids:
rent._compute_lessee_id()
@api.depends('contract_id', 'due_date')
def _compute_lessee_id(self):
for record in self:
record.lessee_id = False
record.company_id = False
if not record.contract_id:
continue
lessee_lines = record.contract_id.lessee_line
valid_lessee_lines = lessee_lines.filtered(
lambda line:
line.start_date and
line.start_date <= record.due_date and #
(not line.end_date or line.end_date >= record.due_date)
)
if valid_lessee_lines and len(valid_lessee_lines) > 0:
record.lessee_id = valid_lessee_lines[0].lessee_id
else:
record.lessee_id = record.contract_id.lessee_id or False
lessor_lines = record.contract_id.lessor_line
valid_lessor_lines = lessor_lines.filtered(
lambda line:
line.start_date and
line.start_date <= record.due_date and
(not line.end_date or line.end_date >= record.due_date)
)
if valid_lessor_lines and len(valid_lessor_lines) > 0:
record.company_id = valid_lessor_lines[0].lessor_id
else:
record.company_id = record.contract_id.lessor_id
@api.depends('contract_id', 'due_date')
def compute_tenure_type(self):
for record in self:
is_deposit = record.management_type_id.name == '押金'
tenure_type = False
first_lease = record.contract_id.lease_ids[:1]
manage_infos = first_lease.address_id.manage_information_ids
if not manage_infos:
record.tenure_type = tenure_type
continue
due_date_dt = fields.Date.from_string(record.due_date)
matched_info = False
if is_deposit:
matched_info = manage_infos.sorted(key=lambda x: x.create_date, reverse=True)[:1]
else:
for info in manage_infos:
start_time = fields.Date.from_string(info.start_time)
if start_time > due_date_dt:
continue
if info.end_date:
end_dt = fields.Date.from_string(info.end_date)
if end_dt < due_date_dt:
continue
matched_info = info
break
if matched_info:
tenure_type = matched_info.tenure_type
record.tenure_type = tenure_type
def update_compute_tenure_type(self):
accounts = self.env["property.accounts.receivable"].browse(
self._context.get('active_ids', self._context.get('active_id', []))
)
for account in accounts:
account.compute_tenure_type()
@api.depends('tenure_type')
def _compute_tenure_type_label(self):
label_map = {'self': '自有', 'agency': '代管'}
for record in self:
record.tenure_type_label = label_map.get(record.tenure_type, '')
def update_charging_range(self):
accounts = self.env["property.accounts.receivable"].browse(
self._context.get('active_ids', self._context.get('active_id', []))
)
for account in accounts:
type_name = account.management_type_id.name or ""
def get_charging_range(start, end):
if not start or not end:
return "" # 日期不全时返回空
return f"{start.strftime('%Y-%m-%d')} 到 {end.strftime('%Y-%m-%d')}"
if type_name in ["租金", "管理费", '灯箱', '管子间']:
contract_actual_end = account.contract_id.actual_end_time
min_end = min(contract_actual_end,
account.due_date_end) if contract_actual_end else account.due_date_end
account.charging_range = get_charging_range(account.due_date, min_end)
elif type_name == "押金":
if account.due_date:
end_date = account.due_date + relativedelta(months=1)
account.charging_range = f"{account.due_date.strftime('%Y-%m-%d')} 到 {end_date.strftime('%Y-%m-%d')}"
elif type_name in ["水费", "电费"]:
if account.due_date:
start_date = (account.due_date - relativedelta(months=2)).replace(day=16)
end_date = (start_date + relativedelta(months=1)).replace(day=14)
account.charging_range = get_charging_range(start_date, end_date)
elif type_name == "违约金":
if account.due_date_end:
account.charging_range = get_charging_range(account.due_date, account.due_date_end)
@api.depends('contract_id')
def _compute_contract_name(self):
for record in self:
if record.contract_id and record.contract_id.version:
record.contract_name = record.contract_id.contract_code + record.contract_id.version
elif record.contract_id:
record.contract_name = record.contract_id.contract_code
else:
record.contract_name = False
@api.depends('company_id')
def _compute_company_id2(self):
for rec in self:
if not rec.company_id:
continue
if rec.company_id:
if rec.company_id.referred == '天河物管':
company = self.env['res.company'].browse(3)
rec.company_id2 = company.id if company else False
elif rec.company_id.referred in ('天河投资', '天投城建'):
company = self.env['res.company'].search([('name', '=', '集团本部')], limit=1)
rec.company_id2 = company.id if company else False
@api.depends('original_amount', 'pre_collection')
def _compute_actual_coping(self):
for record in self:
if record.original_amount:
record.actual_coping = record.original_amount - record.pre_collection
else:
record.actual_coping = 0
def update_lessee_id(self):
# 1. 获取选中的应收账单记录
accounts_ids = self.env["property.accounts.receivable"].browse(
self._context.get('active_ids', self._context.get('active_id'))
)
if not accounts_ids:
return
unique_contracts = {
account.contract_id for account in accounts_ids
if account.contract_id
}
for contract in unique_contracts:
contract_account = self.env['contract.accounts.receivable'].search([
('contract_id', '=', contract.id)
], limit=1)
if not contract_account:
contract_account = self.env['contract.accounts.receivable'].create({
'contract_id': contract.id,
})
contract_account.onchange_contract_id()
else:
contract_account.onchange_contract_id()
@api.depends('management_type_id')
def _compute_is_penalty(self):
for rec in self:
if rec.management_type_id.name == '违约金':
rec.is_penalty = True
else:
rec.is_penalty = False
def penalty_wizard(self):
self.ensure_one()
penalty_lines = []
penalty_lines.append((0, 0, {
'date': self.due_date,
'remake': f'开始计算违约金,应收金额: ¥{self.original_amount}',
'days_overdue': 0,
'remaining_principal': self.original_amount,
'penalty': 0,
}))
return {
'name': '收款明细',
'type': 'ir.actions.act_window',
'res_model': 'penalty.line.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_receivable_id': self.id,
'default_penalty_line_ids': penalty_lines,
'default_rent_collected': self.submitted,
}
}
def _resolve_source_receivable(self):
self.ensure_one()
empty_receivable = self.env['property.accounts.receivable']
if self.source_receivable_id:
return self.source_receivable_id
if self.management_type_id.name != '违约金' or not self.contract_id:
return empty_receivable
date_match = re.search(r'(\d{4}-\d{2}-\d{2})[至-](\d{4}-\d{2}-\d{2})', self.remark or '')
if not date_match:
return empty_receivable
start_date = datetime.strptime(date_match.group(1), "%Y-%m-%d").date()
candidates = self.search([
('contract_id', '=', self.contract_id.id),
('id', '!=', self.id),
('is_invalid', '=', False),
('management_type_id.name', 'not in', ['违约金', '押金']),
('is_liquidated_damages', '=', True),
], order='due_date asc, id asc')
payout_matches = candidates.filtered(
lambda r: r.payout_date and r.payout_date + timedelta(days=1) == start_date
)
if len(payout_matches) == 1:
return payout_matches[:1]
cycle_matches = candidates.filtered(
lambda r: r.due_date and (r.due_date + relativedelta(months=1)).replace(day=1) == start_date
)
if len(cycle_matches) == 1:
return cycle_matches[:1]
if payout_matches:
return payout_matches[:1]
if cycle_matches:
return cycle_matches[:1]
return empty_receivable
def generate_penalty(self):
"""根据选中的租金应收生成违约金应收(仅对已对账部分,逾期判定 + 去重)"""
for receivable in self:
receivable._compute_is_overdue()
if receivable.management_type_id.name != '租金':
continue
if not receivable.is_liquidated_damages:
continue
if not receivable.is_overdue:
continue
if not receivable.contract_id:
continue
percentage = receivable.contract_id.percentage_liquidated_damages
if not percentage or not percentage.name:
continue
original_amount = receivable.original_amount
first_day = (receivable.due_date + relativedelta(months=1)).replace(day=1)
penalty_days = (receivable.payout_date - first_day).days + 1
modify_line = self.env['receivable.modify.line'].search([
('receivable_id', '=', receivable.id),
('reconciliation_date', '!=', False),
('is_penalty', '=', False)
], order='reconciliation_date asc', limit=1)
if receivable.number_of_payments == 1:
original_amount = receivable.original_amount
elif modify_line:
original_amount = receivable.original_amount - modify_line.amount
penalty_amount = round(penalty_days * original_amount * float(percentage.name), 2)
if penalty_amount <= 0:
continue
existing = self.env['property.accounts.receivable'].search([
('contract_id', '=', receivable.contract_id.id),
('management_type_id.name', '=', '违约金'),
('original_amount', '=', penalty_amount),
('source_receivable_id', '=', receivable.id),
('is_invalid', '=', False),
], limit=1)
if existing:
existing.write({
"due_date": receivable.payout_date,
"due_date_end": None,
'is_batch': True,
})
continue
acc_receivable = self.env['property.accounts.receivable'].create({
'contract_id': receivable.contract_id.id,
'management_type_id': self.env['yuthon.management.type'].search(
[('name', '=', '违约金')], limit=1).id,
'due_date': receivable.payout_date,
'due_date_end': None,
'is_batch': True,
'original_amount': penalty_amount,
'remark': f'违约金({first_day}至{receivable.payout_date},共{penalty_days}天)',
'source_receivable_id': receivable.id,
'charging_range': f'{first_day}到{receivable.payout_date}',
'deposit_state': 'unpaid',
})
acc_receivable.update_property_ids()
@api.depends('contract_id')
def _compute_property_name(self):
for record in self:
record.property_name = record.contract_id.use_address
@api.depends('property_ids')
def _compute_title_deed(self):
for record in self:
title_deed = ""
if record.property_ids:
first_property = record.property_ids[0]
title_deed = first_property.title_deed or ""
record.title_deed = title_deed
def invalid(self):
for record in self:
# 处理多对多物业名称,用逗号拼接
property_names = ",".join(record.property_ids.mapped('name')) if record.property_ids else "未知物业"
receivable_log = (
f'当前登录人{self.env.user.name}于{fields.Datetime.now()}将物业为 {property_names}的应收账单状态修改为作废')
original_log = record.receivable_log or ''
new_log = f"{original_log}\n{receivable_log}" if original_log else receivable_log
record.write({
'is_invalid': True,
'receivable_log': new_log
})
return True
# 红冲
def all_red(self):
receivable_ids = self.env["property.accounts.receivable"].browse(
self._context.get('active_ids', self._context.get('active_id', []))
)
for receivable in receivable_ids:
property_names = ",".join(receivable.property_ids.mapped('name')) if receivable.property_ids else "未知物业"
receivable_log = (
f'当前登录人{self.env.user.name}于{fields.Datetime.now()}将物业为 {property_names}的应收账单红冲')
original_log = receivable.receivable_log or ''
new_log = f"{original_log}\n{receivable_log}" if original_log else receivable_log
receivable.write({
'is_red': True,
'red_state_date': fields.Date.today(),
'receivable_log': new_log
})
return True
@api.depends('contract_id')
def _compute_is_refunded(self):
for record in self:
record.is_refunded = False
if record.contract_id:
# 检查是否有已退款的押金记录
refund_bills = self.env['property.bills.list'].search([
('contract_id', '=', record.contract_id.id),
('management_type_id.name', '=', '押金'),
('actual_date', '!=', False),
], limit=1)
record.is_refunded = bool(refund_bills)
@api.depends('contract_id', 'contract_id.lease_ids', 'contract_id.lease_ids.address_id')
def _compute_property_ids(self):
for record in self:
property_ids = self.env['yuthon.property']
if record.contract_id and record.contract_id.lease_ids:
property_ids = record.contract_id.lease_ids.mapped('address_id')
record.property_ids = property_ids
def update_property_ids(self):
receivable_ids = self.env["property.accounts.receivable"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for receivable in receivable_ids:
receivable._compute_property_ids()
@api.onchange('penalty_date')
def _onchange_penalty_date(self):
for record in self:
record.write({
'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:
if receivable.receivable_id.create_time and receivable.penalty_date:
create_time = receivable.receivable_id.create_time
dt = datetime.strptime(str(create_time), '%Y-%m-%d').date()
new_month = dt.month + 1
new_year = dt.year
if new_month > 12:
new_month = 1
new_year += 1
new_date = dt.replace(year=new_year, month=new_month, day=1)
penalty_date = datetime.strptime(str(receivable.penalty_date), '%Y-%m-%d').date()
if penalty_date.year == datetime.now().year - 1:
raise UserWarning("选择的日期为去年的年份,请重新选择!")
days_difference = (penalty_date - new_date).days
penalty = days_difference * receivable.receivable_id.difference_amount
receivable.receivable_id.write({'penalty': penalty})
# 批量退回
def action_refuse_all(self):
model_type_data = {
'水费': 'meter.water.line',
'电费': 'meter.archives.line',
'押金': 'property.deposit.list',
'租金': 'property.rent.list',
'管理费': 'property.management.list',
}
archives_ids = self.env["property.accounts.receivable"].browse(
self._context.get('active_ids', self._context.get('active_id')))
valid_management_types = list(model_type_data.values())
for rec in archives_ids:
if rec.is_reconciliation == True:
return {
"name": "批量退回提醒弹窗",
'type': 'ir.actions.act_window',
'res_model': 'action.refuse.wizard',
"view_mode": "form",
'target': 'new',
'context': {
'default_receivable_ids': archives_ids.ids,
}
}
model_type = model_type_data.get(rec.management_type_id.name)
if model_type not in valid_management_types:
rec.unlink()
continue
if model_type in ('meter.archives.line', 'meter.water.line'):
if not rec.charging_range:
raise UserError("请输入计费区间, 按照应为'YYYY-MM-DD 到 YYYY-MM-DD格式")
charging_range = rec.charging_range.strip()
start_str, end_str = [x.strip() for x in charging_range.split("到")]
start_date = (datetime.strptime(end_str, "%Y-%m-%d").date()).replace(day=1)
end_date = datetime.strptime(end_str, "%Y-%m-%d").date()
model_id = self.env[model_type].search([
('contract_id', '=', rec.contract_id.id),
('meter_date', '>=', start_date),
('meter_date', '<=', end_date)
])
else:
last_day = rec.due_date + relativedelta(months=1) - timedelta(days=1)
model_id = self.env[model_type].search([
('contract_id', '=', rec.contract_id.id),
('due_date', '>=', rec.due_date),
('due_date', '<=', last_day)
])
if model_id:
model_id.write({'is_push': False})
rec.unlink()
def button_accounts_wizard(self):
return {
"type": "ir.actions.act_window",
"name": "操作弹窗",
"res_model": "accounts.wizard",
"view_mode": "form",
"target": "new",
"context": {
"default_accounts_id": self.id
}
}
# 租金
class PropertyRentList(models.Model):
_inherit = 'property.rent.list'
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")
tenure_type = fields.Selection(related='contract_id.tenure_type', string="权属类型", store=True)
property_address = fields.Text(string="物业信息", compute='get_property_address')
contract_sort = fields.Selection([('property', '物业'), ('guanggao', '广告位'),('parking', '停车场'), ('guanzi', '管子间'), ('room', '机房'), ('base_station','基站'), ('other', '其他')], related='contract_id.contract_sort', string="资产类型", index=True)
company_id = fields.Many2one('yuthon.property.company', string="出租方公司", store=True, compute='_compute_company_id')
# 核对人:推送生成应收时写入当前登录人
verifier_id = fields.Many2one('res.users', string="核对人")
@api.depends("property_id", "due_date")
def _compute_admin_id(self):
for record in self:
record.admin_id = False
if not record.property_id or not record.due_date:
continue
manage_lines = record.property_id.manage_information_ids
if not manage_lines:
continue
valid_lines = self.env['manage.information.line']
target_date = record.due_date
for line in manage_lines:
start_valid = line.start_time <= target_date if line.start_time else True
end_valid = True
if line.end_date:
end_valid = target_date <= line.end_date
if start_valid and end_valid:
valid_lines += line
if valid_lines:
valid_lines = valid_lines.sorted(key=lambda l: l.start_time, reverse=True)
record.admin_id = valid_lines[0].admin_id.id
@api.depends('contract_id', 'due_date')
def _compute_company_id(self):
for record in self:
record.company_id = False
if not record.contract_id:
continue
lessor_lines = record.contract_id.lessor_line
valid_lessor_lines = lessor_lines.filtered(
lambda line:
line.start_date and
line.start_date <= record.due_date and
(not line.end_date or line.end_date >= record.due_date)
)
if valid_lessor_lines:
record.company_id = valid_lessor_lines[0].lessor_id
else:
record.company_id = record.contract_id.lessor_id or False
@api.depends('contract_id')
def get_property_address(self):
for rec in self:
rec.property_address = rec.contract_id.property_name
def clear_the_data(self):
archives_ids = self.env["property.rent.list"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in archives_ids:
first_day_of_month = rec.due_date.replace(day=1)
last_day_of_month = first_day_of_month + relativedelta(months=1) - timedelta(days=1)
existing_receivable = self.env['property.accounts.receivable'].search([
('lessee_id', '=', rec.lessee_id.id),
('due_date', '>=', first_day_of_month),
('due_date', '<=', last_day_of_month),
('contract_id', '=', rec.id),
], limit=1)
if existing_receivable:
rec.unlink()
def push_rent(self):
"""推送租金应收到 property.accounts.receivable
如果合同勾选了 is_parking_fee,则应收类型为"停车费"而非"租金"
"""
current_user = self.env.user
is_admin = current_user.has_group('base.group_system') or current_user.has_group('base.group_admin')
for rec in self:
if not is_admin and rec.admin_id.id != current_user.id:
raise UserError(f"您只能推送自己记录")
# 根据合同的 is_parking_fee 标志决定应收类型
if rec.contract_id and rec.contract_id.is_parking_fee:
type_name = '停车费'
else:
type_name = '租金'
management_type = self.env['yuthon.management.type'].search([('name', '=', type_name)], limit=1)
if not management_type:
management_type = self.env['yuthon.management.type'].create({'name': type_name})
due_date = rec.due_date
due_date_end = False
theoretical_end = False
contract = rec.contract_id
if contract and contract.cycle_mode and due_date:
due_date_obj = fields.Date.from_string(due_date)
contract_actual_end = fields.Date.from_string(
contract.actual_end_time) if contract.actual_end_time else None
# 处理自然月
if contract.cycle_mode == 'floating_monthly':
breakdown_line = self.env['property.rent.breakdown.line'].search([
('breakdown_id', '=', contract.id),
('start_data', '<=', due_date),
('end_data', '>=', due_date)
], limit=1)
if breakdown_line:
start_data_obj = fields.Date.from_string(breakdown_line.start_data)
if not (due_date_obj.year == start_data_obj.year and
due_date_obj.month == start_data_obj.month):
due_date = fields.Date.to_string(due_date_obj.replace(day=1))
due_date_obj = fields.Date.from_string(due_date)
last_day = calendar.monthrange(due_date_obj.year, due_date_obj.month)[1]
theoretical_end = date(due_date_obj.year, due_date_obj.month, last_day)
else:
# 处理合同月 (统一先按合同月处理)
# 计算理论应收结束日(当月最后一天)
next_month_first = (due_date_obj + relativedelta(months=1))
theoretical_end = next_month_first - timedelta(days=1)
if contract_actual_end:
due_date_end = fields.Date.to_string(min(theoretical_end, contract_actual_end))
else:
due_date_end = fields.Date.to_string(theoretical_end)
# 创建应收记录
accounts_id = self.env['property.accounts.receivable'].create({
'contract_id': contract.id if contract else False,
'lessee_id': contract.lessee_id.id if contract and contract.lessee_id else None,
'original_amount': rec.original_amount,
'is_liquidated_damages': rec.is_liquidated_damages,
'penalty_ratio_value': rec.percentage_damages,
'due_date': due_date,
'acc_type': rec.acc_type,
'charging_range': f"{due_date} 到 {due_date_end}",
'due_date_end': due_date_end,
'create_time': datetime.now(),
'management_type_id': management_type.id,
})
rec.is_push = True
rec.verifier_id = self.env.user.id # 核对人 = 当前登录人
accounts_id._compute_precinct_id()
accounts_id._compute_lessee_id()
self.reconcile_with_bank_statements(rec)
def batch_push_rent(self):
archives_ids = self.env["property.rent.list"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for rec in archives_ids:
rec.push_rent()
def reconcile_with_bank_statements(self, rec):
bank_accounts = self.env['property.bank.line'].search([
('tenant_id', '=', rec.contract_id.lessee_id.id)
])
if not bank_accounts:
return
if not rec.property_id:
return
# 构建银行对账单搜索条件
bank_domain = [
('name', 'in', [account.bank for account in bank_accounts]),
]
# 查找匹配的银行对账单
matching_statements = self.env['property.bank.statements'].search(bank_domain)
# 更新银行对账单,关联合同和物业
for statement in matching_statements:
# 添加合同ID(如果不存在)
if rec.contract_id.id not in statement.contract_ids.ids:
statement.write({
'contract_ids': [(4, rec.contract_id.id)], # 添加关系
})
# 添加物业ID(如果不存在)
if rec.property_id.id not in statement.property_ids.ids:
statement.write({
'property_ids': [(4, rec.property_id.id)], # 添加关系
})
# 押金
class PropertyDepositList(models.Model):
_inherit = 'property.deposit.list'
contract_id = fields.Many2one('property.lease.contract', string="合同")
due_date = fields.Date(related='contract_id.actual_start_time', string='应收日期', store=True)
posit_type = fields.Selection([('deposit', '押金'), ('performance_bonds', '经营履约保证金'),
('utility_bills', '水电费'), ('decorate', '装修')], string="保证金类型")
original_amount = fields.Float(string="金额")
tenure_type = fields.Selection(related='contract_id.tenure_type', string="权属类型", store=True)
company_id = fields.Many2one(related='contract_id.lessor_id', string="出租方公司", store=True)
# 核对人:推送生成应收时写入当前登录人
verifier_id = fields.Many2one('res.users', string="核对人")
def push_deposit(self):
for rec in self:
management_type_id = self.env['yuthon.management.type'].search([('name', '=', '押金')], limit=1)
if not management_type_id:
management_type_id = self.env['yuthon.management.type'].create({'name': '押金'})
accounts_id = self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'due_date' :rec.due_date + relativedelta(months=1),
'due_date_end': None,
'lessee_id':rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount' : rec.original_amount,
'create_time': datetime.now(),
'management_type_id':management_type_id.id,
'charging_range': f"{rec.due_date.strftime('%Y-%m-%d')} 到 {(rec.due_date + relativedelta(months=1)).strftime('%Y-%m-%d')}",
})
accounts_id._compute_lessee_id()
rec.is_push = True
rec.verifier_id = self.env.user.id # 核对人 = 当前登录人
def batch_push_deposit(self):
archives_ids = self.env["property.deposit.list"].browse(
self._context.get('active_ids', self._context.get('active_id')))
management_type_id = self.env['yuthon.management.type'].search([('name', '=', '押金')], limit=1)
for rec in archives_ids:
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'due_date': rec.due_date,
'lessee_id': rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount': rec.original_amount,
'create_time': datetime.now(),
'management_type_id': management_type_id.id,
})
rec.is_push = True
rec.verifier_id = self.env.user.id # 核对人 = 当前登录人
def delete_duplicate_deposit(self):
"""删除重复押金:同一合同 + 同一保证金类型只保留最早一条,删除其余"""
deposits = self.env['property.deposit.list'].search([
('contract_id', '!=', False),
('posit_type', '!=', False),
])
seen = {}
to_delete = self.env['property.deposit.list']
for dep in deposits.sorted(key=lambda d: (d.contract_id.id, d.posit_type, d.id)):
key = (dep.contract_id.id, dep.posit_type)
if key in seen:
to_delete |= dep
else:
seen[key] = dep.id
count = len(to_delete)
if to_delete:
to_delete.unlink()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '删除重复押金',
'message': f'已删除 {count} 条重复押金记录' if count else '没有重复的押金记录',
'type': 'success' if count else 'info',
'sticky': False,
},
}
# 管理费
class PropertyManagementList(models.Model):
_inherit = 'property.management.list'
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")
property_name = fields.Text(string='物业', compute="_compute_property_name")
company_id = fields.Many2one(related='contract_id.lessor_id', string="出租方公司", store=True)
tenure_type = fields.Selection(related='contract_id.tenure_type', string="权属类型", store=True)
# 核对人:推送生成应收时写入当前登录人
verifier_id = fields.Many2one('res.users', string="核对人")
@api.depends('contract_id')
def _compute_property_name(self):
for record in self:
record.property_name = record.contract_id.property_name
def push_management(self):
type_name_map = {
'management': '管理费',
'guanzijian': '管子间',
'lightbox': '灯箱'
}
for rec in self:
target_name = type_name_map.get(rec.type)
management_type_id = self.env['yuthon.management.type'].search(
[('name', '=', target_name)],
limit=1
)
if not management_type_id:
management_type_id = self.env['yuthon.management.type'].create({'name': target_name})
due_date = rec.due_date
due_date_end = False
theoretical_end = False
contract = rec.contract_id
if contract and contract.cycle_mode and due_date:
due_date_obj = fields.Date.from_string(due_date)
contract_actual_end = fields.Date.from_string(
contract.actual_end_time) if contract.actual_end_time else None
# 处理合同月
if contract.cycle_mode == 'fixed_monthly':
# 计算理论应收结束日(当月最后一天)
next_month_first = (due_date_obj + relativedelta(months=1))
theoretical_end = next_month_first - timedelta(days=1)
# 处理自然月
elif contract.cycle_mode == 'floating_monthly':
breakdown_line = self.env['property.rent.breakdown.line'].search([
('breakdown_id', '=', contract.id),
('start_data', '<=', due_date),
('end_data', '>=', due_date)
], limit=1)
if breakdown_line:
start_data_obj = fields.Date.from_string(breakdown_line.start_data)
if not (due_date_obj.year == start_data_obj.year and
due_date_obj.month == start_data_obj.month):
due_date = fields.Date.to_string(due_date_obj.replace(day=1))
due_date_obj = fields.Date.from_string(due_date)
last_day = calendar.monthrange(due_date_obj.year, due_date_obj.month)[1]
theoretical_end = date(due_date_obj.year, due_date_obj.month, last_day)
if contract_actual_end:
if theoretical_end:
due_date_end = fields.Date.to_string(min(theoretical_end, contract_actual_end))
else:
due_date_end = fields.Date.to_string(contract_actual_end)
else:
due_date_end = fields.Date.to_string(theoretical_end)
accounts_id = self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'lessee_id': rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount': rec.original_amount,
'due_date': rec.due_date,
'due_date_end': due_date_end,
'charging_range': f"{due_date} 到 {due_date_end}",
'create_time': datetime.now(),
'management_type_id': management_type_id.id,
})
accounts_id._compute_lessee_id()
rec.is_push = True
rec.verifier_id = self.env.user.id # 核对人 = 当前登录人
def batch_push_management(self):
archives_ids = self.env["property.management.list"].browse(
self._context.get('active_ids', self._context.get('active_id')))
management_type_id = self.env['yuthon.management.type'].search([('name', '=', '电费')], limit=1)
for rec in archives_ids:
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'lessee_id': rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount': rec.original_amount,
'create_time': datetime.now(),
'management_type_id': management_type_id.id,
})
rec.is_push = True
rec.verifier_id = self.env.user.id # 核对人 = 当前登录人
# 电费
class PropertyTariffList(models.Model):
_inherit = 'property.tariff.list'
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")
admin_id = fields.Many2one(related='contract_id.admin_id', string="片区管理员", store=True)
property_name = fields.Char(related='contract_id.property_name', string="物业信息", store=True)
company_id = fields.Many2one(related='contract_id.lessor_id', string="出租方公司", store=True)
tenure_type = fields.Selection(related='contract_id.tenure_type', string="权属类型", store=True)
def push_tariff(self):
for rec in self:
management_type_id = self.env['yuthon.management.type'].search([('name', '=', '电费')], limit=1)
if not management_type_id:
management_type_id = self.env['yuthon.management.type'].create({'name': '电费'})
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'lessee_id':rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount' : rec.original_amount,
'due_date': rec.due_date,
'create_time': datetime.now(),
'management_type_id':management_type_id.id,
})
rec.is_push = True
def batch_push_tariff(self):
archives_ids = self.env["property.tariff.list"].browse(
self._context.get('active_ids', self._context.get('active_id')))
management_type_id = self.env['yuthon.management.type'].search([('name', '=', '电费')], limit=1)
for rec in archives_ids:
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'lessee_id': rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount': rec.original_amount,
'due_date': rec.due_date,
'create_time': datetime.now(),
'management_type_id': management_type_id.id,
})
rec.is_push = True
# 水费
class PropertyWaterList(models.Model):
_inherit = 'property.water.list'
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")
admin_id = fields.Many2one(related='contract_id.admin_id', string="片区管理员", store=True)
property_name = fields.Char(related='contract_id.property_name', string="物业信息")
company_id = fields.Many2one(related='contract_id.lessor_id', string="出租方公司", store=True)
tenure_type = fields.Selection(related='contract_id.tenure_type', string="权属类型", store=True)
def push_water(self):
for rec in self:
management_type_id = self.env['yuthon.management.type'].search([('name', '=', '水费')], limit=1)
if not management_type_id:
management_type_id = self.env['yuthon.management.type'].create({'name': '水费'})
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'lessee_id': rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount': rec.original_amount,
'create_time': datetime.now(),
'management_type_id': management_type_id.id,
})
rec.is_push = True
def batch_push_water(self):
archives_ids = self.env["property.water.list"].browse(
self._context.get('active_ids', self._context.get('active_id')))
management_type_id = self.env['yuthon.management.type'].search([('name', '=', '水费')], limit=1)
for rec in archives_ids:
self.env['property.accounts.receivable'].create({
'contract_id': rec.contract_id.id,
'lessee_id': rec.contract_id.lessee_id.id if rec.contract_id.lessee_id else None,
'original_amount': rec.original_amount,
'create_time': datetime.now(),
'management_type_id': management_type_id.id,
})
rec.is_push = True
class PropertyPenaltyList(models.Model):
_inherit = 'property.penalty.list'
contract_id = fields.Many2one('property.lease.contract', string="关联租赁合同审批")