Merge pull request !12919 from 李鹏宇/dev
This commit is contained in:
李鹏宇
2026-09-24 11:44:06 +00:00
committed by Gitee
30 changed files with 1219 additions and 135 deletions
@@ -2053,6 +2053,22 @@ class SoongWorkflowMixin(models.AbstractModel):
return record_id.users_id.employee_id
return self.env['hr.employee'] # 默认返回空记录集
def _get_active_workflow_record(self):
"""本单据「未进回收站」的工作流记录(没有则返回空记录集)。
按 res_model + res_id 匹配;回收站里的记录 reclaim_state=True,不算有效记录。
"""
self.ensure_one()
return self.env['soong.workflow.record'].sudo().search([
('res_model', '=', self._name),
('res_id', '=', self.id),
('reclaim_state', '=', False),
], limit=1)
def _has_active_workflow_record(self):
"""本单据是否存在「未进回收站」的工作流记录(回收站里的不算)"""
return bool(self._get_active_workflow_record())
# ---- 公司历史名称展示:web_read 直接改写指向 res.company 的公司字段显示名(见 web_read)----
company_id = fields.Many2one('res.company', string='公司')
@@ -806,6 +806,10 @@ class SoongWorkflowCommentWizard(models.TransientModel):
title=title + '传阅/抄送,测试提醒',
description=description, url=url,
btntxt="查看详情")
# 转交成功:仅这几类单据需要推进业务状态
# (评估申请 -> 物业/方案「评估中」;挂牌申报表 待挂牌 -> 挂牌中;成交确认书 待签约 -> 签约中)
if self.res_model in ('property.evaluate.application', 'property.rental.form', 'property.confirm'):
record_id._on_workflow_transfer()
self._handle_avoidance()
if self.node_btn_name == '退回':
if not self.return_link_id:
+3
View File
@@ -15,6 +15,9 @@
'data/data_yuthon_resource_mgmt_config.xml',
'data/yuthon_property_approval_data.xml',
'data/yuthon_new_property.xml',
# 必须早于 yuthon_property_views.xml:物业列表头部按钮用 %(...)d 引用该动作
'wizard/property_import_wizard_views.xml',
'wizard/property_evaluation_import_wizard_views.xml',
'views/yuthon_property_views.xml',
'views/property_patrol_duty.xml',
'views/property_patrol_plan.xml',
@@ -54,6 +54,14 @@ class PropertyBankStatements(models.Model):
change_amount = fields.Float(string='修改金额')
receivable_modify_ids = fields.One2many('receivable.modify.line', 'bank_id', string='修改应收单')
# 对账(贷)列表排序用:挂账的行排最前面(挂账=0,其余=1),配合「交易时间倒序」使用
bank_state_sort = fields.Integer(string='排序(挂账优先)', compute='_compute_bank_state_sort', store=True)
@api.depends('bank_state')
def _compute_bank_state_sort(self):
for rec in self:
rec.bank_state_sort = 0 if rec.bank_state == 'registered' else 1
def open_record(self):
return {
"type": "ir.actions.act_window",
@@ -9,41 +9,77 @@ class PropertyEvaluationPlan(models.Model):
name = fields.Char(string='物业名称')
evaluation_area = fields.Float(string='评估面积(m²)')
property_ids = fields.Many2many('yuthon.property', string='物业')
property_id = fields.Many2one('yuthon.property', string='物业')
# 业务状态:取关联物业的业务状态(只读展示)
business_state = fields.Selection(related='property_id.business_state', string='业务状态')
company_id = fields.Many2one('yuthon.property.company', string='评估公司', compute='_compute_search', store=True)
admin_id = fields.Many2one('res.users', string="管理员", compute='_compute_search', store=True)
assessment_category = fields.Selection([('rent_money', '租金'), ('value', '价值')], string="评估类别", default='rent_money')
owner_id = fields.Many2one('yuthon.owner.message', string="权属人")
registered_id = fields.Many2one('yuthon.purpose.evidence', string='证载用途')
registered_id2 = fields.Many2one('yuthon.purpose.evidence', string='设定用途')
# 以下字段均从关联物业带出,属计算字段,不落库(口径见 _compute_from_property)
owner_id = fields.Many2one('yuthon.owner.message', string="权属人", compute='_compute_from_property')
registered_id = fields.Many2one('yuthon.purpose.evidence', string='证载用途', compute='_compute_from_property')
registered_id2 = fields.Many2one('yuthon.resource.type', string='设定用途', compute='_compute_from_property')
rent_id = fields.Many2one('rent.free.setting', string="办法")
lease_period = fields.Integer(string='租赁期限(<=)')
first_years_begin = fields.Integer(string='第几年开始')
increments_few_years = fields.Integer(string='每几年递增')
lease_period = fields.Integer(string='租赁期限(<=)', compute='_compute_from_property')
first_years_begin = fields.Integer(string='第几年开始', compute='_compute_from_property')
increments_few_years = fields.Integer(string='每几年递增', compute='_compute_from_property')
lease_form = fields.Selection([('sublease', '分租'), ('whole', '整租')], string="租赁形式", compute='compute_lease_form')
annual_increase_percentage = fields.Float(string='递增百分比', igits=(6, 2))
annual_increase_amount = fields.Float(string='递增金额', igits=(6, 2))
rent_unit = fields.Float(string='递增单价', igits=(6, 2))
annual_increase_percentage = fields.Float(string='递增百分比', igits=(6, 2), compute='_compute_from_property')
annual_increase_amount = fields.Float(string='递增金额', igits=(6, 2), compute='_compute_from_property')
rent_unit = fields.Float(string='递增单价', igits=(6, 2), compute='_compute_from_property')
remake = fields.Text(string='备注')
property_state = fields.Selection([('to', '待审批'), ('done', '已审批')], string="状态", default='to')
property_state = fields.Selection([('to', '待评估'), ('ing', '评估中'), ('done', '已评估')],
string="状态", default='to')
@api.depends('property_ids')
@api.depends('property_id')
def _compute_search(self):
for record in self:
if record.property_ids:
record.company_id = record.property_ids[0].company_id.id
record.admin_id = record.property_ids[0].admin_id.id
if record.property_id:
record.company_id = record.property_id.company_id.id
record.admin_id = record.property_id.admin_id.id
@api.depends('property_ids')
@api.depends('property_id',
'property_id.asset_type_id',
'property_id.reference_ids.start_date',
'property_id.reference_ids.end_date',
'property_id.reference_ids.lease_period',
'property_id.reference_ids.rent_period',
'property_id.reference_ids.first_years_begin',
'property_id.reference_ids.increments_few_years',
'property_id.reference_ids.annual_increase_amount',
'property_id.reference_ids.rent_unit',
'property_id.reference_ids.annual_increase_percentage',
'property_id.title_information_ids.start_time',
'property_id.title_information_ids.end_date',
'property_id.title_information_ids.owner_id',
'property_id.title_information_ids.registered_id')
def _compute_from_property(self):
"""从关联物业带出:
- 租赁期限 / 第几年开始 / 每几年递增 / 递增金额 / 递增单价 / 递增百分比 ← 物业「出租参考条件」中日期范围覆盖当天的那行
- 权属人 / 证载用途 ← 物业「产权信息明细」中日期范围覆盖当天的那行
- 设定用途 ← 物业的「出租类型」(asset_type_id)
"""
for plan in self:
prop = plan.property_id
ref_line = prop._get_valid_reference_line() if prop else prop
title_line = prop._get_valid_title_line() if prop else prop
plan.lease_period = ref_line.lease_period if ref_line else 0
plan.first_years_begin = ref_line.first_years_begin if ref_line else 0
plan.increments_few_years = ref_line.increments_few_years if ref_line else 0
plan.annual_increase_amount = ref_line.annual_increase_amount if ref_line else 0.0
plan.rent_unit = ref_line.rent_unit if ref_line else 0.0
plan.annual_increase_percentage = ref_line.annual_increase_percentage if ref_line else 0.0
plan.owner_id = title_line.owner_id.id if title_line and title_line.owner_id else False
plan.registered_id = title_line.registered_id.id if title_line and title_line.registered_id else False
plan.registered_id2 = prop.asset_type_id.id if prop and prop.asset_type_id else False
@api.depends('property_id')
def compute_lease_form(self):
# 一个方案只对应一个物业 → 恒为「整租」(原「多物业=分租」的判断随 m2o 改造取消)
for record in self:
if record.property_ids:
if len(record.property_ids) > 1:
record.lease_form = 'sublease'
else:
record.lease_form = 'whole'
else:
record.lease_form = False
record.lease_form = 'whole' if record.property_id else False
def put_assessment_approval(self):
@@ -52,12 +88,14 @@ class PropertyEvaluationPlan(models.Model):
form_id = self.env.ref('yuthon_property_approval.property_evaluate_application_form').id
property_list = []
for i in property_evaluation_ids:
if i.property_state == 'done':
raise UserError('该方案不可提交')
# 仅「待评估」可提交评估申请;评估中 / 已评估不再提供该入口
if i.property_state != 'to':
raise UserError('仅「待评估」的方案可以提交评估申请')
property_list.append([0, 0, {
'property_evaluation_id': i.id,
}])
i.write({'property_state': 'done'})
# 此处只负责跳转,状态改为「评估中」由评估申请保存成功时触发
# (见 property.evaluate.application.create)
return {
'name': '提交物业评估',
'view_mode': 'form',
@@ -69,27 +107,60 @@ class PropertyEvaluationPlan(models.Model):
}
}
def action_listing_rental(self):
"""挂牌招租:把所选评估方案关联的物业带进「挂牌申报表」明细行"""
property_evaluation_ids = self.env["property.evaluation.plan"].browse(
self._context.get('active_ids', self._context.get('active_id')))
properties = property_evaluation_ids.mapped('property_id')
if not properties:
raise UserError('所选评估方案未关联物业,无法挂牌招租')
# 已进入签约流程(待签约/签约中/已签约)的物业不放进挂牌申报表
form_model = self.env['property.rental.form']
properties = form_model._get_listable_properties(properties)
if not properties:
raise UserError('所选物业均已进入签约流程(待签约/签约中/已签约),不能在挂牌申报表中提交')
form_model._check_listing_available(properties)
form_id = self.env.ref('yuthon_property_approval.view_property_rental_form_form').id
line_list = []
for prop in properties:
line_list.append([0, 0, {
'property_id': prop.id,
}])
return {
'name': '挂牌招租',
'type': 'ir.actions.act_window',
'res_model': 'property.rental.form',
'view_mode': 'form',
'views': [(form_id, 'form')],
'target': 'current',
'context': {
'default_property_rental_form_line_ids': line_list,
},
}
def button_source(self):
domain = [('id', 'in', self.property_ids.ids)]
"""详情:跳转到对应物业的表单"""
self.ensure_one()
prop = self.property_id
if not prop:
raise UserError('该评估方案未关联物业,无法查看详情')
return {
"name": "物业",
"type": "ir.actions.act_window",
"view_mode": "tree",
"res_model": "yuthon.property",
'tree_view_ref': 'yuthon_property.view_property_evaluation_plan_tree1',
"domain": domain,
"target": "new",
"view_mode": "form",
"views": [[self.env.ref('yuthon_property.yuthon_property_form').id, 'form']],
"target": "current",
"res_id": prop.id,
}
DERIVED_FIELDS = ['owner_id', 'registered_id', 'registered_id2', 'lease_period',
'first_years_begin', 'increments_few_years', 'annual_increase_amount',
'rent_unit', 'annual_increase_percentage']
def update_message(self):
"""这些字段已改为从关联物业实时计算、不落库,这里只需清缓存触发重算"""
property_evaluation_ids = self.env["property.evaluation.plan"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for record in property_evaluation_ids:
property_id = self.sudo().env['yuthon.property'].search([('name', '=', record.name)])
if property_id:
record.write({
'owner_id': property_id.owner_id,
'lease_period': property_id.lease_period,
'registered_id': property_id.registered_id,
})
property_evaluation_ids.invalidate_recordset(self.DERIVED_FIELDS)
+192 -23
View File
@@ -1,7 +1,7 @@
from odoo import models, fields, api
from odoo.exceptions import UserError, ValidationError
from pypinyin import pinyin,lazy_pinyin
from datetime import datetime,date
from datetime import datetime,date,timedelta
import calendar
@@ -39,7 +39,24 @@ class YuthonProperty(models.Model):
string="朝向")
street = fields.Char(string="街道")
property_state = fields.Selection([('on_rent', '在租'), ('vacant', '空置')], string='租赁状态', default='on_rent')
property_leasing = fields.Selection([('to', '待评估'), ('ing', '评估中'), ('done', '已评估'), ('net', '挂网'), ('signing', '签约中')], string='评估状态', default='to')
# 业务状态:评估 → 挂牌 → 签约 的完整链路
# (原先拆成「评估状态 property_leasing」+「挂牌状态 listing_state」两个字段,现合并为一个)
# 流转:保存只置「待X」,转交才置「X中」
business_state = fields.Selection([
('', ''),
('to', '待评估'),
('ing', '评估中'),
('done', '已评估'),
('to_list', '待挂牌'),
('listing', '挂牌中'),
('listed', '已挂牌'),
('to_sign', '待签约'),
('signing', '签约中'),
('signed', '已签约'),
], string='业务状态', default='to')
# 该物业走过的所有审批(soong.workflow.record):展示用,不落库
workflow_record_ids = fields.Many2many('soong.workflow.record', string='审批流程',
compute='_compute_workflow_record_ids')
purpose_id = fields.Many2one('yuthon.resource.purpose', string="管理类型", compute="compute_information_fields", store=True)
is_meter = fields.Boolean(string="水表")
live_room = fields.Many2one('yuthon.live.room', string="居住用房")
@@ -97,6 +114,42 @@ class YuthonProperty(models.Model):
property_type = fields.Selection([('property', '物业'), ('guanggao', '广告位'), ('guanzi', '管子间'), ('room', '机房'), ('base_station','基站'), ('other', '其他')], string="资产类型", default='property')
is_new = fields.Boolean(string="是否最新物业", compute= 'compute_is_new', default=True, store=True)
kingdee_property_id = fields.Char(string="金蝶云物业资产id")
# 基本信息补充(建筑 / 土地)
building_structure = fields.Char(string='建筑结构')
rental_floor = fields.Char(string='出租楼层')
land_nature = fields.Char(string='土地性质')
land_acquisition = fields.Selection([('transfer', '出让'), ('allocation', '划拨')], string='取得方式')
land_end_date = fields.Date(string='土地终止日期')
construction_year = fields.Char(string='建筑年代')
shared_land_area = fields.Float(string='共用地面积(㎡)', digits=(16, 4))
def _evaluation_date_deadline(self):
"""评估基准日对应区间的最后一天:基准日「年份 +1」的同一天再减 1 天
例:基准日 2026-09-24 → 区间 2026-09-24 ~ 2027-09-23
(2 月 29 日在次年不存在时按 2 月 28 日算)
"""
self.ensure_one()
base = self.evaluation_date
if not base:
return False
try:
next_year = base.replace(year=base.year + 1)
except ValueError:
next_year = base.replace(year=base.year + 1, day=28)
return next_year - timedelta(days=1)
@api.onchange('evaluation_date')
def _onchange_evaluation_date(self):
"""修改评估基准日后:若当前日期已超出「基准日 ~ 次年同日 - 1 天」这个区间,
物业业务状态回到「待评估」;业务状态是「已签约」的物业不做处理。
"""
for record in self:
if not record.evaluation_date or record.business_state == 'signed':
continue
deadline = record._evaluation_date_deadline()
if deadline and fields.Date.context_today(record) > deadline:
record.business_state = 'to'
def update_property_state(self):
today = date.today()
@@ -197,17 +250,115 @@ class YuthonProperty(models.Model):
record.is_new = has_valid_line
@api.onchange('reference_ids')
# 「审批流程」页只显示这几类单据(评估审批 / 挂牌审批 / 成交确认书审批)
WORKFLOW_PAGE_MODELS = (
'property.evaluate.application', # 评估审批(物业评估申请)
'property.rental.form', # 挂牌审批(挂牌申报表)
'property.confirm', # 成交确认书审批
)
@api.model
def _get_workflow_source_paths(self, only_models=None):
"""自动发现「挂了工作流、且能关联到本物业」的单据,返回 [(模型名, 到物业的域路径)]
覆盖两层:
① 单据上有指向 yuthon.property 的 Many2one 字段(如 property.split.approval.property_id);
② 单据的明细行(One2many / Many2many)上有指向 yuthon.property 的 Many2one(如
property.rental.form.property_rental_form_line_ids.property_id、
property.confirm.lease_ids.address_id)。
:param only_models: 只在这些模型里找(传 None 表示全库扫描)
"""
mixin_cls = self.env['soong.workflow.mixin'].__class__
paths = []
for model_name in list(self.env.registry):
if model_name not in self.env:
continue
if only_models and model_name not in only_models:
continue
cls = self.env.registry[model_name]
try:
if not issubclass(cls, mixin_cls):
continue
except TypeError:
continue
for fname, field in self.env[model_name]._fields.items():
if field.type == 'many2one' and field.comodel_name == 'yuthon.property':
paths.append((model_name, fname))
elif field.type in ('one2many', 'many2many') and field.comodel_name in self.env:
for sname, sfield in self.env[field.comodel_name]._fields.items():
if sfield.type == 'many2one' and sfield.comodel_name == 'yuthon.property':
paths.append((model_name, '%s.%s' % (fname, sname)))
return paths
@api.depends()
def _compute_workflow_record_ids(self):
"""本物业相关的审批流程记录:只取「评估审批 / 挂牌审批 / 成交确认书审批」且**审批已结束**的"""
Workflow = self.env['soong.workflow.record'].sudo()
paths = {}
for model_name, path in self._get_workflow_source_paths(self.WORKFLOW_PAGE_MODELS):
paths.setdefault(model_name, []).append(path)
for record in self:
found_ids = set()
for model_name, model_paths in paths.items():
source = self.env[model_name].sudo()
res_ids = set()
for path in model_paths:
res_ids.update(source.search([(path, '=', record.id)]).ids)
if res_ids:
found_ids.update(Workflow.search([
('res_model', '=', model_name), ('res_id', 'in', list(res_ids)),
('approve_state', '=', 'end'),
]).ids)
record.workflow_record_ids = Workflow.browse(list(found_ids))
def _get_valid_reference_line(self):
"""取出租参考条件明细行:优先取 [开始日期, 结束日期] 覆盖当天的那条;
没有命中时回退到原有行为(最后一条)。"""
self.ensure_one()
lines = self.reference_ids
if not lines:
return lines
today = fields.Date.context_today(self)
valid = lines.filtered(
lambda line: (not line.start_date or line.start_date <= today)
and (not line.end_date or line.end_date >= today)
)
if not valid:
return lines[-1:]
# 多条命中时取开始日期最新的一条
return valid.sorted(lambda line: line.start_date or today, reverse=True)[:1]
def _get_valid_title_line(self):
"""取产权信息明细行:优先取 [生效日期, 失效日期] 覆盖当天的那条;
没有命中时回退到最后一条。"""
self.ensure_one()
lines = self.title_information_ids
if not lines:
return lines
today = fields.Date.context_today(self)
valid = lines.filtered(
lambda line: (not line.start_time or line.start_time <= today)
and (not line.end_date or line.end_date >= today)
)
if not valid:
return lines[-1:]
# 多条命中时取生效日期最新的一条
return valid.sorted(lambda line: line.start_time or today, reverse=True)[:1]
@api.onchange('reference_ids', 'reference_ids.start_date', 'reference_ids.end_date')
def _onchange_reference_ids(self):
for record in self:
if record.reference_ids:
record.lease_period = record.reference_ids[-1].lease_period
record.rent_period = record.reference_ids[-1].rent_period
record.first_years_begin = record.reference_ids[-1].first_years_begin
record.increments_few_years = record.reference_ids[-1].increments_few_years
record.annual_increase_percentage = record.reference_ids[-1].annual_increase_percentage
record.annual_increase_amount = record.reference_ids[-1].annual_increase_amount
record.rent_unit = record.reference_ids[-1].rent_unit
line = record._get_valid_reference_line()
if line:
record.lease_period = line.lease_period
record.rent_period = line.rent_period
record.first_years_begin = line.first_years_begin
record.increments_few_years = line.increments_few_years
record.annual_increase_percentage = line.annual_increase_percentage
record.annual_increase_amount = line.annual_increase_amount
record.rent_unit = line.rent_unit
else:
record.lease_period = 0.0
record.rent_period = 0.0
@@ -386,6 +537,13 @@ class YuthonProperty(models.Model):
evaluation_rent_total = fields.Float(string='评估总价(元•月,散租)')
evaluation_whole_rent_price = fields.Float(string='评估整租单价(元/m² •月)')
evaluation_whole_rent_total = fields.Float(string='评估总价(元•月,整租)')
# 评估信息页(物业表单「评估信息」tab)
evaluation_date = fields.Date(string='基准日')
evaluation_info = fields.Char(string='评估信息')
evaluation_agency = fields.Char(string='评估机构')
evaluation_code = fields.Char(string='评估编号')
evaluation_method = fields.Char(string='评估方法')
evaluation_remark = fields.Char(string='备注')
def update_precinct(self):
property_ids = self.env["yuthon.property"].browse(
@@ -397,6 +555,12 @@ class YuthonProperty(models.Model):
def put_declare_approval(self):
property_ids = self.env["yuthon.property"].browse(
self._context.get('active_ids', self._context.get('active_id')))
# 已进入签约流程(待签约/签约中/已签约)的物业不放进挂牌申报表
form_model = self.env['property.rental.form']
property_ids = form_model._get_listable_properties(property_ids)
if not property_ids:
raise UserError('所选物业均已进入签约流程(待签约/签约中/已签约),不能在挂牌申报表中提交')
form_model._check_listing_available(property_ids)
form_id = self.env.ref('yuthon_property_approval.view_property_rental_form_form').id
property_list = []
for record in property_ids:
@@ -498,9 +662,10 @@ class YuthonProperty(models.Model):
def put_assessment_approval(self):
for record in self:
ref_line = record._get_valid_reference_line()
self.env['property.evaluation.plan'].create({
'name': record.name,
'property_ids': [(6, 0, [record.id])],
'property_id': record.id,
'evaluation_area': record.rent_area,
'owner_id': record.owner_id.id if record.owner_id else None,
'registered_id': record.registered_id.id if record.registered_id else None,
@@ -509,7 +674,7 @@ class YuthonProperty(models.Model):
'increments_few_years': record.increments_few_years,
'annual_increase_amount': record.annual_increase_amount,
'rent_unit': record.rent_unit,
'annual_increase_percentage': record.reference_ids[-1].annual_increase_percentage if record.reference_ids else None,
'annual_increase_percentage': ref_line.annual_increase_percentage if ref_line else None,
'assessment_category': 'rent_money',
'remake': record.remark,
})
@@ -525,11 +690,12 @@ class YuthonProperty(models.Model):
property_ids = self.env["yuthon.property"].browse(
self._context.get('active_ids', self._context.get('active_id')))
for record in property_ids:
if record.property_leasing == 'ing':
if record.business_state == 'ing':
raise UserError(f'{record.name}该物业已在评估,请勿重复提交')
ref_line = record._get_valid_reference_line()
self.env['property.evaluation.plan'].create({
'name': record.name,
'property_ids': [(6, 0, [record.id])],
'property_id': record.id,
'evaluation_area': record.rent_area,
'owner_id': record.owner_id.id if record.owner_id else None,
'registered_id': record.registered_id.id if record.registered_id else None,
@@ -538,11 +704,11 @@ class YuthonProperty(models.Model):
'increments_few_years': record.increments_few_years,
'annual_increase_amount': record.annual_increase_amount,
'rent_unit': record.rent_unit,
'annual_increase_percentage': record.reference_ids[-1].annual_increase_percentage if record.reference_ids else None,
'annual_increase_percentage': ref_line.annual_increase_percentage if ref_line else None,
'assessment_category': 'rent_money',
'remake': record.remark,
})
record.write({'property_leasing': 'ing'})
record.write({'business_state': 'ing'})
return {
"name": "物业评估方案",
'type': 'ir.actions.act_window',
@@ -612,12 +778,13 @@ class YuthonProperty(models.Model):
precinct_id = self.env["yuthon.precinct.message"].search([('employee_id.name', '=', record.owner_id.name)])
owner_id = record.title_information_ids[-1].owner_id.id if record.title_information_ids else ''
company_id = record.manage_information_ids[-1].company_id.id if record.manage_information_ids else ''
lease_period = record.reference_ids[-1].lease_period if record.reference_ids else ''
rent_period = record.reference_ids[-1].rent_period if record.reference_ids else ''
first_years_begin = record.reference_ids[-1].first_years_begin if record.reference_ids else ''
increments_few_years = record.reference_ids[-1].increments_few_years if record.reference_ids else ''
annual_increase_amount = record.reference_ids[-1].annual_increase_amount if record.reference_ids else ''
rent_unit = record.reference_ids[-1].rent_unit if record.reference_ids else ''
ref_line = record._get_valid_reference_line()
lease_period = ref_line.lease_period if ref_line else ''
rent_period = ref_line.rent_period if ref_line else ''
first_years_begin = ref_line.first_years_begin if ref_line else ''
increments_few_years = ref_line.increments_few_years if ref_line else ''
annual_increase_amount = ref_line.annual_increase_amount if ref_line else ''
rent_unit = ref_line.rent_unit if ref_line else ''
title_address = record.title_information_ids[-1].property_message_id if record.title_information_ids else ''
title_deed = record.title_information_ids[-1].title_certificate if record.title_information_ids else ''
registered_id = record.title_information_ids[-1].registered_id.id if record.title_information_ids else ''
@@ -905,6 +1072,8 @@ class YuthonReferenceConditions(models.Model):
_description = '出租参考条件'
yuthon_property_id = fields.Many2one('yuthon.property', string="关联物业信息")
start_date = fields.Date(string='开始日期')
end_date = fields.Date(string='结束日期')
lease_period = fields.Integer(string='租赁期限(<=)')
rent_period = fields.Integer(string="免租期(<=)")
first_years_begin = fields.Integer(string='第几年开始')
@@ -105,3 +105,5 @@ access_utility_meter_wizard,utility.meter.wizard,model_utility_meter_wizard,base
access_invoicing_information_line,invoicing.information.line,model_invoicing_information_line,base.group_user,1,1,1,1
access_tenant_import_wizard,tenant.import.wizard,model_tenant_import_wizard,base.group_user,1,1,1,1
access_batch_invoice_export_wizard,batch.invoice.export.wizard,model_batch_invoice_export_wizard,base.group_user,1,1,1,1
access_property_import_wizard,property.import.wizard,model_property_import_wizard,base.group_user,1,1,1,1
access_property_evaluation_import_wizard,property.evaluation.import.wizard,model_property_evaluation_import_wizard,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
105 access_batch_invoice_export_wizard batch.invoice.export.wizard model_batch_invoice_export_wizard base.group_user 1 1 1 1
106 access_property_import_wizard property.import.wizard model_property_import_wizard base.group_user 1 1 1 1
107 access_property_evaluation_import_wizard property.evaluation.import.wizard model_property_evaluation_import_wizard base.group_user 1 1 1 1
108
109
+16
View File
@@ -24,6 +24,22 @@
parent="yuthon_property_menu"
action="view_property_evaluation_plan_action"
/>
<!-- 评估方案子级:按状态拆成三个菜单 -->
<menuitem id="yuthon_property_evaluation_plan_menu_todo"
sequence="1"
name="待评估"
parent="yuthon_property_evaluation_plan_menu"
action="view_property_evaluation_plan_action_todo"/>
<menuitem id="yuthon_property_evaluation_plan_menu_ing"
sequence="2"
name="评估中"
parent="yuthon_property_evaluation_plan_menu"
action="view_property_evaluation_plan_action_ing"/>
<menuitem id="yuthon_property_evaluation_plan_menu_done"
sequence="3"
name="已评估"
parent="yuthon_property_evaluation_plan_menu"
action="view_property_evaluation_plan_action_done"/>
<menuitem id="yuthon_property_evaluation_experts_menu"
sequence="20"
name="评估专家库(二期)"
@@ -23,6 +23,34 @@
</field>
</record>
<!-- 对账(贷)专用列表:交易时间倒序 + 挂账的排最前面。
独立 primary 视图(inherit_id 为空),避免污染其它菜单用的 view_property_bank_statements_tree -->
<record id="view_property_bank_statements_tree_loan" model="ir.ui.view">
<field name="name">property.bank.statements.tree.loan</field>
<field name="model">property.bank.statements</field>
<field name="mode">primary</field>
<field name="priority">99</field>
<field name="arch" type="xml">
<tree string="对账" editable="bottom" js_class="custom_statements_list_button" edit="1" delete="false" limit="15"
default_order="bank_state_sort, start_time desc"
decoration-warning="bank_state == 'registered'">
<field name="bank_state_sort" column_invisible="1"/>
<field name="start_time" />
<field name="original_amount"/>
<field name="yes_amount" string="已对账"/>
<field name="no_amount"/>
<field name="remake"/>
<field name="extract"/>
<field name="name_account" />
<field name="your_own_unit"/>
<field name="property_ids" widget="many2many_tags" column_invisible="1"/>
<field name="bank_state" />
<field name="workflow_record_id" column_invisible="1"/>
<button name="open_record" string="流程" type="object" class="btn-primary" invisible="not workflow_record_id"/>
</tree>
</field>
</record>
<record id="view_property_bank_statements_tree1" model="ir.ui.view">
<field name="name">property.bank.statements.tree</field>
<field name="model">property.bank.statements</field>
@@ -98,7 +126,7 @@
<field name="view_mode">tree,search</field>
<field name="domain">[('bank_state', 'in', ['not_reconciled', 'registered']), ('lending_direction', '=', 'loan')]</field>
<field name="view_ids" eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_property.view_property_bank_statements_tree')})]"/>
(0, 0, {'view_mode': 'tree', 'view_id': ref('yuthon_property.view_property_bank_statements_tree_loan')})]"/>
</record>
<record id="view_property_bank_statements_action_borrow" model="ir.actions.act_window">
<field name="name">对账(借)</field>
@@ -8,22 +8,19 @@
<header>
<button name="put_assessment_approval" type="object" string="提交评估申请" class="btn btn-primary"/>
</header>
<field name="name"/>
<field name="property_ids" widget="many2many_tags" optional="hide" options="{'no_create': True}"/>
<field name="property_id" optional="hide" options="{'no_create': True}"/>
<field name="evaluation_area"/>
<field name="assessment_category"/>
<field name="owner_id"/>
<field name="registered_id"/>
<field name="registered_id2"/>
<field name="lease_period"/>
<field name="rent_id"/>
<field name="first_years_begin"/>
<field name="increments_few_years"/>
<field name="annual_increase_amount"/>
<field name="rent_unit"/>
<field name="annual_increase_percentage"/>
<field name="owner_id"/>
<field name="registered_id"/>
<field name="registered_id2"/>
<field name="remake"/>
<field name="property_state"/>
<field name="lease_form" optional="hide"/>
<button name="button_source" type="object" string="详情" class="btn btn-blue"/>
</tree>
@@ -49,6 +46,93 @@
<field name="name">物业评估方案</field>
<field name="res_model">property.evaluation.plan</field>
<field name="view_mode">tree,search</field>
<!-- 显式清空:升级只更新 XML 中出现的字段,直接删掉不会清 DB 里的旧 context(该字段 NOT NULL,不能写 False) -->
<field name="context">{}</field>
</record>
<!-- ⚠️ 列表 header 按钮的显隐只能按「列表上下文」整体评估(不逐行),且 Odoo 合并视图时
会把所有继承视图一并叠加(_get_inheriting_views 无 mode 过滤),继承方案会污染基础视图。
因此「评估中 / 已评估」必须用 **独立视图**(不设 inherit_id),各自定义 header 按钮。 -->
<!-- 评估中:无任何 header 按钮 -->
<record id="view_property_evaluation_plan_tree_ing" model="ir.ui.view">
<field name="name">property.evaluation.plan.tree.ing</field>
<field name="model">property.evaluation.plan</field>
<field name="inherit_id" eval="False"/>
<field name="mode">primary</field>
<!-- 优先级调后,避免被 default_view 选为模型默认列表视图 -->
<field name="priority">99</field>
<field name="arch" type="xml">
<tree string="物业评估方案" editable="bottom" create="false" js_class="custom_evaluation_button">
<field name="property_id" optional="hide" options="{'no_create': True}"/>
<field name="evaluation_area"/>
<field name="assessment_category"/>
<field name="lease_period"/>
<field name="first_years_begin"/>
<field name="increments_few_years"/>
<field name="annual_increase_amount"/>
<field name="rent_unit"/>
<field name="annual_increase_percentage"/>
<field name="owner_id"/>
<field name="registered_id"/>
<field name="registered_id2"/>
<field name="remake"/>
<field name="lease_form" optional="hide"/>
<button name="button_source" type="object" string="详情" class="btn btn-blue"/>
</tree>
</field>
</record>
<!-- 已评估:header 提供「挂牌招租」 -->
<record id="view_property_evaluation_plan_tree_done" model="ir.ui.view">
<field name="name">property.evaluation.plan.tree.done</field>
<field name="model">property.evaluation.plan</field>
<field name="inherit_id" eval="False"/>
<field name="mode">primary</field>
<!-- 优先级调后,避免被 default_view 选为模型默认列表视图 -->
<field name="priority">99</field>
<field name="arch" type="xml">
<tree string="物业评估方案" editable="bottom" create="false" js_class="custom_evaluation_button">
<header>
<button name="action_listing_rental" type="object" string="挂牌招租" class="btn btn-primary"/>
</header>
<field name="business_state" optional="show"/>
<field name="property_id" optional="hide" options="{'no_create': True}"/>
<field name="evaluation_area"/>
<field name="assessment_category"/>
<field name="lease_period"/>
<field name="first_years_begin"/>
<field name="increments_few_years"/>
<field name="annual_increase_amount"/>
<field name="rent_unit"/>
<field name="annual_increase_percentage"/>
<field name="owner_id"/>
<field name="registered_id"/>
<field name="registered_id2"/>
<field name="remake"/>
<field name="lease_form" optional="hide"/>
<button name="button_source" type="object" string="详情" class="btn btn-blue"/>
</tree>
</field>
</record>
<!-- 按状态拆分的三个子菜单:待评估 / 评估中 / 已评估 -->
<record id="view_property_evaluation_plan_action_todo" model="ir.actions.act_window">
<field name="name">待评估</field>
<field name="res_model">property.evaluation.plan</field>
<field name="view_mode">tree,search</field>
<field name="domain">[('property_state', '=', 'to')]</field>
<field name="view_id" ref="view_property_evaluation_plan_tree"/>
</record>
<record id="view_property_evaluation_plan_action_ing" model="ir.actions.act_window">
<field name="name">评估中</field>
<field name="res_model">property.evaluation.plan</field>
<field name="view_mode">tree,search</field>
<field name="domain">[('property_state', '=', 'ing')]</field>
<field name="view_id" ref="view_property_evaluation_plan_tree_ing"/>
</record>
<record id="view_property_evaluation_plan_action_done" model="ir.actions.act_window">
<field name="name">已评估</field>
<field name="res_model">property.evaluation.plan</field>
<field name="view_mode">tree,search</field>
<field name="domain">[('property_state', '=', 'done')]</field>
<field name="view_id" ref="view_property_evaluation_plan_tree_done"/>
</record>
<record id="all_property_update_message" model="ir.actions.server">
<field name="name">更新信息</field>
+60 -11
View File
@@ -7,10 +7,12 @@
<tree string="物业管理" multi_edit="1">
<header>
<button name="put_merge_approval" string="合并" type="object" class="btn btn-primary"/>
<button name="%(action_property_import_wizard)d" string="物业导入" type="action" class="btn btn-primary"/>
<button name="%(action_property_evaluation_import_wizard)d" string="评估信息导入" type="action" class="btn btn-primary"/>
<button name="put_split_approval" type="object" string="拆分" class="btn btn-primary"/>
<button name="put_property_handover" string="物业移交" type="object" class="btn btn-primary"/>
<button name="put_declare_approval" type="object" string="提交出租申报" class="btn btn-primary"/>
<button name="all_assessment_approval" string="评估推送" type="object" class="btn btn-blue"/>
<button name="all_assessment_approval" string="评估推送" type="object" class="btn btn-primary"/>
<button name="put_property_management" string="物业变更" type="object" class="btn btn-primary"/>
</header>
<field name="name" required="1" class="list_name_width"/>
@@ -20,14 +22,14 @@
<field name="title_deed"/>
<field name="title_address" optional="hide"/>
<field name="property_state"/>
<field name="property_leasing"/>
<field name="business_state"/>
<field name="purpose_id"/>
<field name="property_type"/>
<field name="is_new" column_invisible="1"/>
<field name="first_letter" column_invisible="1"/>
<button name="put_assessment_approval" type="object" string="评估推送" class="btn btn-primary" invisible="property_leasing == 'ing'"/>
<button name="put_offline_approval" type="object" string="提交线下审批" class="btn btn-primary" invisible="property_leasing != 'done'"/>
<button name="put_adjustment_approval" type="object" string="调整出租条件" class="btn btn-primary" invisible="property_leasing != 'done'"/>
<button name="put_assessment_approval" type="object" string="评估推送" class="btn btn-primary" invisible="business_state in ('ing', 'done')"/>
<button name="put_offline_approval" type="object" string="提交线下审批" class="btn btn-primary" invisible="business_state not in ('to_list', 'listing', 'listed', 'to_sign', 'signing', 'signed')"/>
<button name="put_adjustment_approval" type="object" string="调整出租条件" class="btn btn-primary" invisible="business_state not in ('to_list', 'listing', 'listed', 'to_sign', 'signing', 'signed')"/>
</tree>
</field>
</record>
@@ -66,10 +68,17 @@
<field name="financial_address"/>
<field name="effective_date"/>
<field name="kingdee_property_id"/>
<field name="building_structure"/>
<field name="rental_floor"/>
<field name="land_nature"/>
<field name="land_acquisition"/>
<field name="land_end_date"/>
<field name="construction_year"/>
<field name="shared_land_area"/>
</group>
<group>
<field name="property_state" groups="soong_workflow.group_soong_workflow_manager"/>
<field name="property_leasing" groups="soong_workflow.group_soong_workflow_manager"/>
<field name="business_state" groups="soong_workflow.group_soong_workflow_manager"/>
<field name="purpose_id" groups="soong_workflow.group_soong_workflow_manager"/>
<field name="property_type" groups="soong_workflow.group_soong_workflow_manager"/>
<field name="company_id" invisible="1"/>
@@ -208,6 +217,8 @@
<field name="annual_increase_amount"/>
<field name="rent_unit"/>
<field name="annual_increase_percentage"/>
<field name="start_date"/>
<field name="end_date"/>
</tree>
</field>
</page>
@@ -268,6 +279,38 @@
</tree>
</field>
</page>
<page string="评估信息" name="evaluation_info_page">
<group>
<group>
<field name="evaluation_date"/>
<field name="evaluation_rent_price" string="分租单价"/>
<field name="evaluation_rent_total" string="分租总价"/>
<field name="evaluation_whole_rent_price" string="整租单价"/>
<field name="evaluation_whole_rent_total" string="整租总价"/>
</group>
<group>
<field name="evaluation_info"/>
<field name="evaluation_agency"/>
<field name="evaluation_code"/>
<field name="evaluation_method"/>
<field name="evaluation_remark"/>
</group>
</group>
</page>
<page string="审批流程" name="workflow_record_page">
<field name="workflow_record_ids" nolabel="1">
<tree create="false" edit="false" delete="false"
default_order="create_date desc">
<field name="code" string="流水号"/>
<field name="workflow_id" string="审批类型"/>
<field name="name" string="名称"/>
<field name="create_uid" string="发起人"/>
<field name="create_date" string="发起时间"/>
<field name="display_end_time" string="办结时间"/>
<button name="open_record" type="object" string="详情" class="btn btn-primary"/>
</tree>
</field>
</page>
</notebook>
</sheet>
<!-- <div class="oe_chatter">-->
@@ -289,17 +332,23 @@
<group expand="0">
<filter name="group_property_floor" string="楼层" context="{'group_by': 'property_floor_id'}"/>
<filter name="group_statu" string="租赁状态" context="{'group_by': 'property_state'}"/>
<filter name="group_property_leasing" string="评估状态" context="{'group_by': 'property_leasing'}"/>
<filter name="group_business_state" string="业务状态" context="{'group_by': 'business_state'}"/>
<filter name="purpose_id" string="管理类型" context="{'group_by': 'purpose_id'}"/>
<filter name="admin_id" string="管理员" context="{'group_by': 'admin_id'}"/>
<filter name="title_address" string="产权地址" context="{'group_by': 'title_address'}"/>
<filter name="referred_id" string="地段" context="{'group_by': 'referred_id'}"/>
</group>
<header_search>
<quick_filter string="状态">
<filter string="待评估" name="property_state" domain="[('property_leasing', '=', 'to')]" />
<filter string="评估中" name="done" domain="[('property_leasing', '=', 'ing')]"/>
<filter string="已评估" name="to_do" domain="[('property_leasing', '=', 'done')]" />
<quick_filter string="业务状态">
<filter string="待评估" name="bs_to" domain="[('business_state', '=', 'to')]" />
<filter string="评估中" name="bs_ing" domain="[('business_state', '=', 'ing')]"/>
<filter string="已评估" name="bs_done" domain="[('business_state', '=', 'done')]" />
<filter string="待挂牌" name="bs_to_list" domain="[('business_state', '=', 'to_list')]" />
<filter string="挂牌中" name="bs_listing" domain="[('business_state', '=', 'listing')]" />
<filter string="已挂牌" name="bs_listed" domain="[('business_state', '=', 'listed')]" />
<filter string="待签约" name="bs_to_sign" domain="[('business_state', '=', 'to_sign')]" />
<filter string="签约中" name="bs_signing" domain="[('business_state', '=', 'signing')]" />
<filter string="已签约" name="bs_signed" domain="[('business_state', '=', 'signed')]" />
</quick_filter>
</header_search>
<searchpanel>
+2
View File
@@ -13,3 +13,5 @@ from . import task_push_wizard
from . import utility_meter_wizard
from . import tenant_import_wizard
from . import batch_invoice_export_wizard
from . import property_import_wizard
from . import property_evaluation_import_wizard
@@ -6,26 +6,18 @@ class EvaluationPlanWizard(models.Model):
_name = 'evaluation.plan.wizard'
_description = '评估方案弹窗'
property_ids = fields.Many2many('yuthon.property', string='物业')
# 一个评估方案对应一个物业(原为多选 m2m,已改为 m2o)
property_id = fields.Many2one('yuthon.property', string='物业')
def confirm(self):
evaluation_area = 0
name_list = []
first_id = self.property_ids[0]
for i in self.property_ids:
name = i.name
name_list.append(name)
evaluation_area += i.evaluation_area
name = ','.join(name_list)
self.ensure_one()
prop = self.property_id
if not prop:
raise UserError('请先选择物业')
self.env['property.evaluation.plan'].create({
'name': name,
'name': prop.name,
'property_id': prop.id,
'assessment_category': 'rent_money',
'evaluation_area': evaluation_area,
'owner_id': first_id.title_information_ids[0].owner_id.id if first_id.title_information_ids else None,
'registered_id': first_id.title_information_ids[0].registered_id.id if first_id.title_information_ids else None,
'rent_id': first_id.reference_ids[0].rent_id.id if first_id.reference_ids else None,
'lease_period': first_id.reference_ids[0].lease_period if first_id.reference_ids else None,
'annual_increase_percentage': first_id.reference_ids[0].annual_increase_percentage if first_id.reference_ids else None,
'remake': first_id.remark,
'property_ids': self.property_ids.ids
'evaluation_area': prop.evaluation_area,
'remake': prop.remark,
})
@@ -7,7 +7,8 @@
<form>
<sheet>
<group>
<field name="property_ids" widget="many2many_tags" domain="[('property_state', '=', 'to')]"/>
<field name="property_id" options="{'no_create': True}"
domain="[('property_state', '=', 'to')]"/>
</group>
</sheet>
<footer>
@@ -66,4 +66,18 @@
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
</record>
<!-- 合同执行中、距实际结束日期不足 3 个月 → 物业业务状态回到「待评估」(每份合同只推送一次) -->
<record forcecreate="True" id="cron_contract_expire_to_evaluate" model="ir.cron">
<field name="name">定时任务:合同到期前3个月推送物业评估</field>
<field name="model_id" ref="model_property_lease_contract"/>
<field name="state">code</field>
<field name="code">model.task_contract_expire_to_evaluate()</field>
<field eval="True" name="active"/>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="nextcall" eval="(datetime.now() + relativedelta(hour=9, minute=10, second=0)).strftime('%Y-%m-%d %H:%M:%S')"/>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
</record>
</odoo>
@@ -125,9 +125,8 @@ class YuthonProperty(models.Model):
for record in self.search([('property_state', '=', 'to'), ('property_contract_ids', '=', False)]):
contracts = self.env['property.evaluation.plan'].search([])
for contract in contracts:
for property_id in contract.property_ids:
if property_id.id not in unique_property_ids:
unique_property_ids.add(property_id.id)
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:
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, exceptions, _
from odoo import models, fields, api, exceptions, _, Command
from odoo.exceptions import UserError
class PropertyConfirm(models.Model):
@@ -36,12 +37,116 @@ class PropertyConfirm(models.Model):
# 递增百分比
annual_increase_percentage = fields.Float(string="递增百分比", digits=(6, 2))
# 「经办人 / 审批状态」取自本单据的工作流记录(口径同 property.lease.contract)
employee_ids = fields.Many2many('hr.employee', string="当前经办人", compute='_compute_workflow')
approve_state = fields.Selection([("start", "草稿"), ("pending", "审批中"), ("end", "结束")],
string="审批状态", compute='_compute_workflow')
# 业务状态:取物业明细「第一个」行的物业业务状态(只读展示)
business_state = fields.Selection(
related='lease_ids.address_id.business_state', string='业务状态')
@api.depends('code')
def _compute_workflow(self):
for record in self:
workflow = record._get_active_workflow_record()
record.employee_ids = workflow.employee_ids if workflow else False
record.approve_state = workflow.approve_state if workflow else False
@api.depends('lease_ids', 'lease_ids.property_area')
def _compute_property_area(self):
# 汇总每条成交确认单下所有物业明细行的可租面积
for rec in self:
rec.property_area = sum(rec.lease_ids.mapped('property_area'))
@staticmethod
def _x2m_lease_property_ids(commands):
"""从「物业列表」的一对多命令里取出物业 id(address_id)"""
ids = set()
for cmd in commands or []:
if cmd[0] == Command.CREATE and len(cmd) > 2 and cmd[2].get('address_id'):
ids.add(cmd[2]['address_id'])
return list(ids)
@api.model
def _check_no_duplicate_application(self, properties):
"""前置校验:同一物业不能重复生成成交确认书
(已存在「未进回收站」的成交确认书即视为已申请过)"""
records = self.env['property.confirm'].search([('lease_ids.address_id', 'in', properties.ids)])
alive = records.filtered(lambda rec: rec._has_active_workflow_record())
duplicated = alive.mapped('lease_ids.address_id') & properties
if duplicated:
raise UserError('以下物业已生成过成交确认书(未进回收站),不能重复申请:%s'
% '、'.join(duplicated.mapped('name')))
@api.model_create_multi
def create(self, vals_list):
# 先校验(在建表之前):同一物业不能重复生成成交确认书
prop_ids = []
for vals in vals_list:
prop_ids += self._x2m_lease_property_ids(vals.get('lease_ids'))
if prop_ids:
self._check_no_duplicate_application(self.env['yuthon.property'].browse(set(prop_ids)))
records = super().create(vals_list)
# 保存时业务状态为「待签约」;推进到「签约中」是在**转交**时(见 _on_workflow_transfer)
# 只向前推进:已处于「待签约/签约中/已签约」的不再改动,避免把流程回退
props = records.mapped('lease_ids.address_id').filtered(
lambda prop: prop.business_state not in ('to_sign', 'signing', 'signed'))
if props:
props.write({'business_state': 'to_sign'})
return records
def _on_workflow_transfer(self):
"""转交(工作流弹窗触发,仅挂牌申报表/成交确认书):关联物业业务状态「待签约」→「签约中」"""
props = self.mapped('lease_ids.address_id').filtered(
lambda prop: prop.business_state in (False, '', 'to_sign'))
if props:
props.write({'business_state': 'signing'})
def action_create_contract(self):
"""创建合同:把成交确认书的物业与条件带进租赁合同
字段对应关系:
- 物业 → 合同「物业信息明细行」(`lease_ids.address_id`)
- 面积 → 合同「总面积」(`total_area`)
- 租期年限 → 合同「租期(年)」(`lease_term_year`)
- 免租天数 → 合同「免租期 月/日」;**31 天算 1 个月**(如 32 天 → 1 月 1 日)
- 每 X 年递增 Y% → 合同「每几年递增」+ 递增方式=百分比 + 递增值
(对应合同递增组合字段的后半部分,不涉及「自第 X 年开始」)
"""
self.ensure_one()
# 只有审批状态为「结束」的成交确认书才能创建合同(列表按钮同样按此显隐)
if self.approve_state != 'end':
raise UserError('只有审批状态为「结束」的成交确认书才能创建合同')
properties = self.lease_ids.mapped('address_id')
if not properties:
raise UserError('该成交确认书未关联物业,无法创建合同')
form_id = self.env.ref('yuthon_property_approval.view_property_lease_contract_form').id
# 免租天数换算成「月/日」:31 天 = 1 个月;余数为 0 时借一个月,避免出现「X月0日」
# 例:31 天 → 0月31日;32 天 → 1月1日;62 天 → 1月31日
free_months, free_days = divmod(self.rent_free_days or 0, 31)
if free_months and not free_days:
free_months -= 1
free_days = 31
return {
'name': '合同审批',
'type': 'ir.actions.act_window',
'res_model': 'property.lease.contract',
'view_mode': 'form',
'views': [(form_id, 'form')],
'target': 'current',
'context': {
'default_lease_ids': [(0, 0, {'address_id': prop.id}) for prop in properties],
'default_total_area': self.property_area,
'default_lease_term_year': self.lease_term_years,
'default_rent_free_period_month': free_months,
'default_rent_free_period_days': free_days,
'default_increments_every_few_years': self.increments_few_years,
'default_increment_method': 'percentage',
'default_increment_value': self.annual_increase_percentage,
},
}
def _get_default_name_title(self):
return '成交确认书'
@@ -1,4 +1,11 @@
from email.policy import default
import base64
import io
from copy import copy
from openpyxl import load_workbook
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
from odoo.tools.misc import file_path
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
@@ -26,7 +33,6 @@ class PropertyEvaluateApplication(models.Model):
active = fields.Boolean('Active', default=True, tracking=True)
date_assessment = fields.Date(string='评估基准日')
evaluation_company_id = fields.Many2one('evaluation.company', string='评估公司')
quote = fields.Float(string='报价')
service_money = fields.Float(string='评估服务费总额')
pay_time = fields.Date(string='支付时间')
pay_way = fields.Char(string='支付方式')
@@ -62,6 +68,261 @@ class PropertyEvaluateApplication(models.Model):
}
}
# ================= 导出「资产评估结果明细表」 =================
# 模板:static/others/assessment_result_template.xlsx(单工作表「租金-结果明细表」,25 列)
# 结构:第 1~3 行 标题/评估基准日/委托人;第 4 行 评估编号·评估方法·评估信息(评估机构填写);
# 第 5~6 行 两级表头;其后为数据区,再依次是「合计」行、备注说明、调整表、落款(评估人员/评估机构)。
# 行号不写死:运行时按「A 列为『序号』的第一行」定位表头、「A 列为『合计』」定位合计行。
EXPORT_TEMPLATE_PATH = 'yuthon_property_approval/static/others/assessment_result_template.xlsx'
EXPORT_COL_COUNT = 25
@staticmethod
def _find_assessment_data_start(sheet, max_row):
"""定位数据区起始行:A 列为「序号」的是第一级表头,其下含「证载面积」的是第二级表头"""
first_header = None
for row in range(1, max_row + 1):
if str(sheet.cell(row, 1).value or '').strip() == '序号':
first_header = row
break
if first_header is None:
raise UserError('导出模板格式异常:未找到表头行')
for row in range(first_header, max_row + 1):
if '证载面积' in str(sheet.cell(row, 5).value or ''):
return row + 1
return first_header + 2
@staticmethod
def _fmt_num(value):
"""整数去掉小数点,其余按 %g 输出"""
if value in (False, None, ''):
return ''
try:
number = float(value)
except (TypeError, ValueError):
return str(value)
return str(int(number)) if number == int(number) else ('%g' % number)
@api.model
def _selection_label(self, model_name, field_name, value):
if not value:
return ''
field = self.env[model_name]._fields.get(field_name)
if not field:
return value
return dict(field._description_selection(self.env)).get(value, value)
@api.model
def _increase_text(self, ref_line):
"""租金递增文案,取自物业「出租参考条件」中覆盖当天的那行。
递增金额 / 递增单价 / 递增百分比 **只会填其中一个**,按填了哪个拼单位;
若历史数据里同时填了多个,按 百分比 → 金额 → 单价 的优先级取第一个。
每几年递增 > 1 时写「每N年递增」,否则写「每年递增」。
"""
if not ref_line:
return ''
if ref_line.annual_increase_percentage:
value, unit = ref_line.annual_increase_percentage, '%'
elif ref_line.annual_increase_amount:
value, unit = ref_line.annual_increase_amount, '元'
elif ref_line.rent_unit:
value, unit = ref_line.rent_unit, '元/㎡'
else:
return ''
few_years = ref_line.increments_few_years or 0
every = '每年' if few_years <= 1 else '每%s年' % few_years
text = '%s递增%s%s' % (every, self._fmt_num(value), unit)
if ref_line.first_years_begin:
text = '第%s年起%s' % (ref_line.first_years_begin, text)
return text
def _build_assessment_export_rows(self):
"""按物业明细行生成 25 列的数据行;最后 5 列(分租/整租单价总价、备注)留空"""
rows = []
for index, line in enumerate(self.property_ids, start=1):
prop = line.property_id
ref_line = prop._get_valid_reference_line() if prop else prop
title_line = prop._get_valid_title_line() if prop else prop
certificate = ''
if title_line and title_line.property_message_id:
certificate = title_line.property_message_id.title_certificate or ''
rows.append([
index, # A 序号
prop.name or '', # B 地址及位置
title_line.owner_id.name if title_line and title_line.owner_id else '', # C 权属人/产权持有者
certificate, # D 权证编号
title_line.equity_area if title_line and title_line.equity_area else None, # E 证载面积(㎡)
title_line.registered_id.name if title_line and title_line.registered_id else '', # F 证载用途
prop.building_structure or '', # G 建筑结构
prop.rental_floor or '', # H 出租楼层
prop.property_floor_id.show_name or '' if prop.property_floor_id else '', # I 总楼层
prop.construction_year or '', # J 建筑年代
prop.shared_land_area or None, # K 共用地面积(㎡)
prop.land_nature or '', # L 土地性质
self._selection_label('yuthon.property', 'land_acquisition', prop.land_acquisition), # M 取得方式
self._selection_label('yuthon.property', 'property_type', prop.property_type), # N 用途
prop.land_end_date or None, # O 土地终止日期
line.evaluation_area or None, # P 出租/评估面积(㎡)
prop.asset_type_id.name or '' if prop.asset_type_id else '', # Q 出租用途
'不超过%s年' % self._fmt_num(ref_line.lease_period) if ref_line and ref_line.lease_period else '', # R 租期
'不超过%s天' % self._fmt_num(ref_line.rent_period) if ref_line and ref_line.rent_period else '', # S 免租期
self._increase_text(ref_line), # T 租金递增
None, None, None, None, None, # U~Y 留空(评估结果 + 备注)
])
return rows
def action_export_assessment_result(self):
"""导出资产评估结果明细表(按模板生成 xlsx 并下载)"""
self.ensure_one()
if not self.property_ids:
raise UserError('该评估申请没有物业明细行,无法导出')
try:
template_path = file_path(self.EXPORT_TEMPLATE_PATH)
except FileNotFoundError:
raise UserError('未找到导出模板文件,请检查模块文件是否完整')
workbook = load_workbook(template_path)
sheet = workbook[workbook.sheetnames[0]]
col_count = self.EXPORT_COL_COUNT
max_row = sheet.max_row
# ① 定位表头(A 列为「序号」的是第一级表头行,其下含「证载面积」的是第二级表头行),
# 数据区紧随表头之后;再定位「合计」行,其后的内容(备注说明 / 调整表 / 落款)整体搬移
data_start = self._find_assessment_data_start(sheet, max_row)
total_row = None
for row in range(data_start, max_row + 1):
if str(sheet.cell(row, 1).value or '').strip() == '合计':
total_row = row
break
if total_row is None:
raise UserError('导出模板格式异常:未找到「合计」行')
data_styles = {col: copy(sheet.cell(data_start, col)._style) for col in range(1, col_count + 1)}
total_styles = {col: copy(sheet.cell(total_row, col)._style) for col in range(1, col_count + 1)}
# 以「合计」行为原点 0,把它和它之后的内容(备注说明 / 调整表 / 落款)整体记录、稍后搬移
tail = {} # (相对行, 列) -> (值, 样式)
tail_heights = {} # 相对行 -> 行高
tail_last_offset = 0
for offset, row in enumerate(range(total_row, max_row + 1)):
tail_heights[offset] = sheet.row_dimensions[row].height
for col in range(1, col_count + 1):
cell = sheet.cell(row, col)
tail[(offset, col)] = (cell.value, copy(cell._style))
if cell.value is not None:
tail_last_offset = offset
tail_merges = [
(r.min_row - total_row, r.min_col, r.max_row - total_row, r.max_col)
for r in sheet.merged_cells.ranges
if r.min_row >= total_row
]
# ② 解除数据区以后的合并,并彻底清空数据区到表尾
for name in [str(r) for r in sheet.merged_cells.ranges if r.min_row >= data_start]:
sheet.unmerge_cells(name)
for row in range(data_start, max_row + 1):
for col in range(1, col_count + 1):
cell = sheet.cell(row, col)
cell.value = None
cell.border = Border()
cell.fill = PatternFill()
cell.font = Font()
cell.alignment = Alignment()
# ③ 写入数据行(每行一个物业,样式沿用模板数据行,边框统一补齐)
data_rows = self._build_assessment_export_rows()
side = Side(style='thin')
full_border = Border(left=side, right=side, top=side, bottom=side)
for offset, values in enumerate(data_rows):
row = data_start + offset
for col in range(1, col_count + 1):
cell = sheet.cell(row, col)
cell._style = copy(data_styles[col])
cell.value = values[col - 1]
cell.border = full_border
if not sheet.row_dimensions[row].height or sheet.row_dimensions[row].height < 20:
sheet.row_dimensions[row].height = 30
# ④ 把「合计」行及其后的固定内容整体下移到数据行之后
total_target = data_start + len(data_rows)
for (offset, col), (value, style) in tail.items():
cell = sheet.cell(total_target + offset, col)
cell._style = copy(style)
cell.value = value
for offset, height in tail_heights.items():
if height:
sheet.row_dimensions[total_target + offset].height = height
for min_row, min_col, max_row_, max_col in tail_merges:
sheet.merge_cells(
start_row=total_target + min_row, start_column=min_col,
end_row=total_target + max_row_, end_column=max_col)
# ⑤ 合计行的面积两列按实际行数求和(金额列留空)
last_data_row = data_start + len(data_rows) - 1
for col in range(1, col_count + 1):
sheet.cell(total_target, col)._style = copy(total_styles[col])
sheet.cell(total_target, 1, value='合计')
sheet.cell(total_target, 5, value='=SUM(E%s:E%s)' % (data_start, last_data_row))
sheet.cell(total_target, 16, value='=SUM(P%s:P%s)' % (data_start, last_data_row))
# ⑥ 删掉模板残留的空行,避免表格下方拖出一大片空白
tail_len = tail_last_offset + 1
first_empty_row = total_target + tail_len
if sheet.max_row >= first_empty_row:
sheet.delete_rows(first_empty_row, sheet.max_row - first_empty_row + 1)
# ⑦ 文档级信息:评估基准日 / 委托人(取表单上的,模板里的示例值一律覆盖)
base_date = self.date_assessment
sheet.cell(2, 1, value='评估基准日:%s' % (base_date.strftime('%Y年%m月%d日') if base_date else ''))
sheet.cell(3, 1, value='委托人:%s' % (self.users_id.name or ''))
# ⑧ 落款:评估人员留空(由评估机构填),评估机构取表单上的「评估公司」
company_name = self.evaluation_company_id.name or ''
for row in range(total_target, sheet.max_row + 1):
for col in range(1, col_count + 1):
value = sheet.cell(row, col).value
if not isinstance(value, str):
continue
if value.startswith('评估人员:'):
sheet.cell(row, col, value='评估人员:')
elif value.startswith('评估机构:'):
sheet.cell(row, col, value='评估机构:%s' % company_name)
output = io.BytesIO()
workbook.save(output)
output.seek(0)
file_name = '资产评估结果明细表-%s.xlsx' % (self.code or self.name or self.id)
attachment = self.env['ir.attachment'].create({
'name': file_name,
'type': 'binary',
'datas': base64.b64encode(output.read()),
'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
return {
'type': 'ir.actions.act_url',
'url': '/web/content/%s?download=true' % attachment.id,
'target': 'self',
}
def _on_workflow_transfer(self):
"""「转交」(含非退回的加签)成功后的业务钩子。
与挂牌申报表 / 成交确认书同一口径:**保存只置「待X」,转交才置「X中」**。
评估申请真正流转时才把关联的评估方案与物业置为「评估中」,单纯保存不算。
触发点见 soong_workflow/wizard/soong_workflow_comment_wizard.py。
"""
for application in self:
plans = application.property_ids.mapped('property_evaluation_id')
waiting_plans = plans.filtered(lambda plan: plan.property_state == 'to')
if waiting_plans:
waiting_plans.write({'property_state': 'ing'})
# 物业自己的业务状态同步为「评估中」,不动已是「评估中/已评估及之后」的物业
waiting_props = plans.mapped('property_id').filtered(
lambda prop: prop.business_state in (False, '', 'to'))
if waiting_props:
waiting_props.write({'business_state': 'ing'})
@api.onchange('is_issues')
def _onchange_is_issues(self):
if self.is_issues:
@@ -84,7 +345,7 @@ class YuhtonPropertyLine(models.Model):
property_message_id = fields.Many2one('property.evaluate.application', string='关联评估申请')
property_evaluation_id = fields.Many2one('property.evaluation.plan', string="关联物业评估方案")
property_ids = fields.Many2many(related='property_evaluation_id.property_ids', string="物业信息")
property_id = fields.Many2one(related='property_evaluation_id.property_id', string="物业信息")
owner_id = fields.Many2one('yuthon.owner.message', related='property_evaluation_id.owner_id', string="权属人")
assessment_category = fields.Selection(related='property_evaluation_id.assessment_category', string="评估类别")
lease_period = fields.Integer(related='property_evaluation_id.lease_period', string='租期期限(<=)')
@@ -103,7 +364,7 @@ class YuhtonPropertyLine(models.Model):
"view_mode": "tree",
"res_model": "yuthon.property",
"target": "new",
"domain": [('id', 'in', self.property_ids.ids)],
"domain": [('id', '=', self.property_id.id)],
}
@@ -9,6 +9,9 @@ from numpy.ma.core import default_filler
import re
from odoo import models, fields, api, exceptions, _
from odoo.exceptions import UserError, ValidationError
import logging
_logger = logging.getLogger(__name__)
class PropertyContractLine(models.Model):
@@ -216,6 +219,9 @@ class PropertyLeaseContract(models.Model):
term_end_time = fields.Date(string="合约结束时间")
actual_start_time = fields.Date(string="实际开始时间")
actual_end_time = fields.Date(string="实际结束时间")
# 距实际结束日期不足 3 个月时,每日任务把物业业务状态推回「待评估」;
# 该字段记录推送日期,保证每份合同只推送一次(不会被反复重置)
evaluation_pushed_date = fields.Date(string="评估推送日期", copy=False)
end_comparison = fields.Selection([('red', '红'), ('yellow', '黄'), ('blue', '蓝')], string="结束时间对比(颜色)", compute='_compute_end_comparison', store=True)
expiration_coming = fields.Selection([('coming', '即将到期'), ('due', '已到期')], string="结束时间对比(状态)", compute='_compute_expiration_coming', store=True)
is_equal = fields.Boolean(string="是否相等", compute='_compute_end_comparison', store=True)
@@ -1006,6 +1012,13 @@ class PropertyLeaseContract(models.Model):
'actual_start_time': self.term_start_time,
'actual_end_time': self.term_end_time,
})
# 审批结束:合同明细行上的物业 -> 已签约
signed_props = self.mapped('lease_ids.address_id').filtered(
lambda addr: addr.id and addr.business_state != 'signed')
if signed_props:
signed_props.write({'business_state': 'signed'})
_logger.info('合同审批结束:物业置为已签约 %s', signed_props.ids)
old_contract_id = self.env['property.lease.contract'].search([
('contract_code', '=', self.contract_code),
('id', '!=', self.id),
@@ -2265,6 +2278,35 @@ class PropertyLeaseContract(models.Model):
for contract in executing_contracts:
contract.update_contact_state()
def task_contract_expire_to_evaluate(self):
"""每日任务:正在执行、且距「实际结束时间」不足 3 个月的合同,
把其「物业信息明细行」上物业的业务状态置为「待评估」。
- 只找 contact_state = 执行中 的合同;
- 实际结束日期落在 [今天, 今天 + 90 天] 之内(已到期的合同不处理);
- 每份合同只推送一次(用 evaluation_pushed_date 记录),避免物业已经进入
评估 / 挂牌流程后又被每天重置回「待评估」。
"""
today = fields.Date.today()
deadline = today + timedelta(days=90)
contracts = self.search([
('contact_state', '=', 'ing'),
('actual_end_time', '!=', False),
('actual_end_time', '>=', today),
('actual_end_time', '<=', deadline),
('evaluation_pushed_date', '=', False),
])
if not contracts:
return
for contract in contracts:
properties = contract.lease_ids.mapped('address_id')
if properties:
properties.write({'business_state': 'to'})
contract.evaluation_pushed_date = today
_logger.info('合同到期前评估推送:合同 %s(实际结束 %s)→ 物业 %s 置为待评估',
contract.contract_code or contract.id, contract.actual_end_time,
properties.ids)
def update_contact_state(self):
for contract in self:
receivable_records = self.env['property.accounts.receivable'].search([
@@ -2500,7 +2542,7 @@ class PropertyLeaseContract(models.Model):
rent_area += lease_id.address_id.rent_area
self.env['property.evaluation.plan'].create({
'property_name': record.property_name,
'property_ids': [(6, 0, property_ids)],
'property_id': property_ids[0] if property_ids else None,
'evaluation_area': rent_area,
'owner_id': first_address_id.title_information_ids[0].owner_id.id if first_address_id and first_address_id.title_information_ids else None,
'registered_id': first_address_id.title_information_ids[0].registered_id.id if first_address_id and first_address_id.title_information_ids else None,
@@ -2531,7 +2573,7 @@ class PropertyLeaseContract(models.Model):
rent_area += lease_id.address_id.rent_area
self.env['property.evaluation.plan'].create({
'property_name': record.property_name,
'property_ids': property_ids,
'property_id': property_ids[0] if property_ids else None,
'evaluation_area': property_id.rent_area,
'owner_id': property_id.title_information_ids[0].owner_id.id if property_id.title_information_ids else None,
'registered_id': property_id.title_information_ids[0].registered_id.id if property_id.title_information_ids else None,
@@ -47,9 +47,17 @@ class PropertyMeetingApproval(models.Model):
})
def button_rental(self):
# 已进入签约流程(待签约/签约中/已签约)的物业不放进挂牌申报表
form_model = self.env['property.rental.form']
lines = self.meeting_line_ids.filtered(
lambda line: line.property_id
and form_model._get_listable_properties(line.property_id))
if not lines:
raise UserError('所选物业均已进入签约流程(待签约/签约中/已签约),不能在挂牌申报表中提交')
form_model._check_listing_available(lines.mapped('property_id'))
property_list = []
form_id = self.env.ref('yuthon_property_approval.view_property_rental_form_form').id
for record in self.meeting_line_ids:
for record in lines:
property_list.append([0, 0, {
'property_id': record.property_id.id,
}])
@@ -1,4 +1,9 @@
from odoo import models, fields, api
from odoo import models, fields, api, Command
from odoo.exceptions import UserError
# 已进入签约流程的业务状态:这些物业不再放入挂牌申报表
SIGN_FLOW_STATES = ('to_sign', 'signing', 'signed')
class PropertyRentalForm(models.Model):
@@ -29,6 +34,120 @@ class PropertyRentalForm(models.Model):
name_title = fields.Char(default=_get_default_name_title)
self_users_ids = fields.Many2many('res.users', string="当前经办人", default=lambda self: [(6, 0, [self.env.user.id])])
# 「经办人 / 审批状态」取自本单据的工作流记录(口径同 property.lease.contract)
employee_ids = fields.Many2many('hr.employee', string="当前经办人", compute='_compute_workflow')
approve_state = fields.Selection([("start", "草稿"), ("pending", "审批中"), ("end", "结束")],
string="审批状态", compute='_compute_workflow')
# 业务状态:取物业明细「第一个」行的物业业务状态(只读展示)
business_state = fields.Selection(
related='property_rental_form_line_ids.property_id.business_state', string='业务状态')
@api.depends('code')
def _compute_workflow(self):
for record in self:
workflow = record._get_active_workflow_record()
record.employee_ids = workflow.employee_ids if workflow else False
record.approve_state = workflow.approve_state if workflow else False
@api.model
def _get_listable_properties(self, properties):
"""剔除已进入签约流程(待签约 / 签约中 / 已签约)的物业 —— 这些不放进挂牌申报表"""
return properties.filtered(lambda prop: prop.business_state not in SIGN_FLOW_STATES)
@api.model
def _check_listing_available(self, properties):
"""挂牌申请的前置校验:
① 物业尚未进入挂牌流程(业务状态为 挂牌中/已挂牌 时不可再提交;待签约/签约中/已签约
已由 `_get_listable_properties()` 在入口处剔除,此处作兜底);
② 同一物业不能重复申请 —— 已存在「未进回收站」的挂牌申报表即视为已申请过。
"""
labels = dict(self.env['yuthon.property']._fields['business_state'].selection)
locked_states = ('listing', 'listed', 'to_sign', 'signing', 'signed')
not_ready = properties.filtered(lambda prop: prop.business_state in locked_states)
if not_ready:
detail = '、'.join(
'%s(%s)' % (prop.name, labels.get(prop.business_state) or '未设置')
for prop in not_ready)
raise UserError('以下物业已进入挂牌/签约流程,不能提交挂牌申请:%s' % detail)
duplicated = self._find_duplicated_properties(properties)
if duplicated:
raise UserError('以下物业已提交过挂牌申请(未进回收站),不能重复申请:%s'
% '、'.join(duplicated.mapped('name')))
@api.model
def _find_duplicated_properties(self, properties):
"""已存在「未进回收站」的挂牌申报表的物业(判断依据:该单据有有效的工作流记录)"""
forms = self.env['property.rental.form'].search([
('property_rental_form_line_ids.property_id', 'in', properties.ids),
])
alive = forms.filtered(lambda form: form._has_active_workflow_record())
return alive.mapped('property_rental_form_line_ids.property_id') & properties
@api.model_create_multi
def create(self, vals_list):
# 已进入签约流程(待签约 / 签约中 / 已签约)的物业**不放进挂牌申报表**:在建表前直接剔除这些明细行
prop_ids = []
for vals in vals_list:
cmds = vals.get('property_rental_form_line_ids') or []
kept = []
for cmd in cmds:
if cmd[0] == Command.CREATE and len(cmd) > 2 and cmd[2].get('property_id'):
prop = self.env['yuthon.property'].browse(cmd[2]['property_id'])
if prop.business_state in SIGN_FLOW_STATES:
continue
prop_ids.append(prop.id)
kept.append(cmd)
if len(kept) != len(cmds):
if not kept:
raise UserError('所选物业均已进入签约流程(待签约/签约中/已签约),不能在挂牌申报表中提交')
vals['property_rental_form_line_ids'] = kept
# 再校验(在建表之前):已进入挂牌/签约流程的物业不能提交
if prop_ids:
self._check_listing_available(self.env['yuthon.property'].browse(prop_ids))
forms = super().create(vals_list)
# 保存时业务状态为「待挂牌」;推进到「挂牌中」是在**转交**时(见 _on_workflow_transfer)
props = forms.mapped('property_rental_form_line_ids.property_id')
waiting = props.filtered(
lambda prop: prop.business_state not in ('listing', 'listed', 'to_sign', 'signing', 'signed'))
if waiting:
waiting.write({'business_state': 'to_list'})
return forms
def _on_workflow_transfer(self):
"""转交(工作流弹窗触发,仅挂牌申报表/成交确认书):关联物业业务状态「待挂牌」→「挂牌中」"""
props = self.mapped('property_rental_form_line_ids.property_id')
waiting = props.filtered(lambda prop: prop.business_state in (False, '', 'to_list'))
if waiting:
waiting.write({'business_state': 'listing'})
def end_approval(self):
"""审批结束:关联物业的业务状态推进为「已挂牌」"""
props = self.mapped('property_rental_form_line_ids.property_id')
waiting = props.filtered(lambda prop: prop.business_state in (False, '', 'listing'))
if waiting:
waiting.write({'business_state': 'listed'})
def action_transaction_confirmed(self):
"""成交确认书:把本单的物业带进「成交确认审批」(property.confirm) 的物业列表"""
self.ensure_one()
properties = self.property_rental_form_line_ids.mapped('property_id')
if not properties:
raise UserError('该挂牌申报表未关联物业,无法生成成交确认书')
form_id = self.env.ref('yuthon_property_approval.view_property_confirm_form').id
return {
'name': '成交确认审批',
'type': 'ir.actions.act_window',
'res_model': 'property.confirm',
'view_mode': 'form',
'views': [(form_id, 'form')],
'target': 'current',
'context': {
'default_lease_ids': [(0, 0, {'address_id': prop.id}) for prop in properties],
},
}
class PropertyRentalFormLine(models.Model):
_name = 'property.rental.form.line'
@@ -4,8 +4,12 @@ from odoo.exceptions import UserError, ValidationError
class PropertyTransactionConfirmed(models.Model):
"""⚠️ 已作废:成交确认书业务已迁到 property.confirm(成交确认审批)。
模型与视图保留仅供历史数据查看,菜单/按钮均已不再指向本模型,
挂牌状态推进与「创建合同」逻辑已移除(现由 property.confirm 负责)。
"""
_name = 'property.transaction.confirmed'
_description = '成交确认书'
_description = '成交确认书(已作废)'
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin', 'soong.workflow.mixin']
name = fields.Char(string='标题')
@@ -32,6 +36,17 @@ class PropertyTransactionConfirmed(models.Model):
name_title = fields.Char(default=_get_default_name_title)
self_users_ids = fields.Many2many('res.users', string="当前经办人", default=lambda self: [(6, 0, [self.env.user.id])])
# 「经办人 / 审批状态」取自本单据的工作流记录(口径同 property.lease.contract)
employee_ids = fields.Many2many('hr.employee', string="当前经办人", compute='_compute_workflow')
approve_state = fields.Selection([("start", "草稿"), ("pending", "审批中"), ("end", "结束")],
string="审批状态", compute='_compute_workflow')
@api.depends('code')
def _compute_workflow(self):
for record in self:
workflow = record._get_active_workflow_record()
record.employee_ids = workflow.employee_ids if workflow else False
record.approve_state = workflow.approve_state if workflow else False
@api.onchange('is_issues')
def _onchange_is_issues(self):
@@ -5,8 +5,18 @@
<field name="model">property.confirm</field>
<field name="arch" type="xml">
<tree string="成交确认">
<field name="name"/>
<field name="code"/>
<field name="name" string="标题"/>
<field name="property_name"/>
<field name="lessee"/>
<field name="property_area"/>
<field name="lease_term_years"/>
<field name="rent_free_days"/>
<field name="employee_ids" widget="many2many_tags"/>
<field name="business_state" string="业务状态"/>
<field name="approve_state" string="审批状态" widget="badge"
decoration-danger="approve_state == 'end'" decoration-success="approve_state == 'pending'"/>
<button name="action_create_contract" type="object" string="创建合同" class="btn btn-primary"
invisible="approve_state != 'end'"/>
</tree>
</field>
</record>
@@ -50,21 +50,30 @@
<group>
<field name="users_id" invisible="1"/>
<field name="department_id" invisible="1"/>
<field name="quote"/>
<field name="evaluation_company_id"/>
<field name="date_assessment"/>
<field name="service_money"/>
<field name="pay_time"/>
<field name="pay_way"/>
<!-- 左右两栏:左 3 个 / 右 3 个 -->
<group>
<field name="evaluation_company_id"/>
<field name="date_assessment"/>
<field name="service_money"/>
</group>
<group>
<field name="pay_time"/>
<field name="pay_way"/>
</group>
<field name="is_experts"/>
</group>
<notebook>
<page string="待评估物业出租方案明细表">
<div class="mb-2">
<button name="action_export_assessment_result" type="object"
string="导出" class="btn-primary"
invisible="not property_ids"/>
</div>
<field name="property_ids">
<tree editable="bottom">
<field name="property_message_id" column_invisible="1"/>
<field name="property_evaluation_id" class="list_width" string="物业"/>
<field name="property_ids" widget="many2many_tags" optional="hide"/>
<field name="property_id" optional="hide"/>
<field name="evaluation_area"/>
<field name="assessment_category"/>
<field name="owner_id"/>
@@ -151,13 +151,6 @@
action="property_confirm_action"
sequence="5"/>
<menuitem
id="property_assess_approval_menu"
parent="yuthon_property.yuthon_property_menu"
name="评估申请"
action="property_evaluate_application_action"
sequence="7"/>
<menuitem
id="property_record_approval_menu"
parent="yuthon_property.yuthon_property_menu"
@@ -200,11 +193,12 @@
parent="yuthon_property.yuthon_property_menu_data"
action="property_offline_registration_action"/>
<!-- 成交确认书菜单指向「成交确认审批」(property.confirm);原 property.transaction.confirmed 已作废 -->
<menuitem id="yuthon_property_confirmation_of_transaction_menu"
sequence="5"
name="成交确认书"
parent="yuthon_property.yuthon_property_menu_data"
action="action_property_transaction_confirmed"/>
action="property_confirm_action"/>
<menuitem id="property_rental_conditions_menu"
sequence="10"
@@ -4,7 +4,7 @@
<field name="name">property.merge.approval.tree</field>
<field name="model">property.merge.approval</field>
<field name="arch" type="xml">
<tree string="合并管理">
<tree string="合并管理" default_order="start_date desc">
<field name="property_name"/>
<field name="code"/>
<field name="merge_asset_type_id"/>
@@ -5,8 +5,14 @@
<field name="model">property.rental.form</field>
<field name="arch" type="xml">
<tree string="挂牌申报表">
<field name="name"/>
<field name="code"/>
<field name="name"/>
<field name="listing_method"/>
<field name="business_state" string="业务状态"/>
<field name="employee_ids" widget="many2many_tags"/>
<field name="approve_state" string="审批状态" widget="badge"
decoration-danger="approve_state == 'end'" decoration-success="approve_state == 'pending'"/>
<button name="action_transaction_confirmed" type="object" string="成交确认书" class="btn btn-primary"/>
</tree>
</field>
</record>
@@ -51,7 +57,8 @@
<field name="property_rental_form_line_ids">
<tree editable="bottom">
<field name="property_rental_form_id" column_invisible="1"/>
<field name="property_id" options="{'no_create': True}"/>
<field name="property_id" options="{'no_create': True}"
domain="[('business_state', 'not in', ['to_sign', 'signing', 'signed'])]"/>
<field name="registered_id" optional="hide"/>
<field name="evaluation_area" optional="hide"/>
<field name="current_status"/>
@@ -77,5 +84,9 @@
<field name="name">挂牌申报表</field>
<field name="res_model">property.rental.form</field>
<field name="view_mode">tree,form</field>
<!-- 已进入签约阶段(待签约 / 签约中 / 已签约)的不在挂牌申报表列表显示;
尚未填明细行的空单仍然显示,避免刚保存的表单从列表消失 -->
<field name="domain">['|', ('property_rental_form_line_ids', '=', False),
('business_state', 'not in', ['to_sign', 'signing', 'signed'])]</field>
</record>
</odoo>
@@ -4,7 +4,7 @@
<field name="name">property.split.approval.tree</field>
<field name="model">property.split.approval</field>
<field name="arch" type="xml">
<tree string="物业拆分审批">
<tree string="物业拆分审批" default_order="effective_date desc">
<field name="property_id"/>
<field name="code"/>
<field name="units"/>
@@ -8,6 +8,9 @@
<field name="name"/>
<field name="lessee_id"/>
<field name="entity_type"/>
<field name="employee_ids" widget="many2many_tags"/>
<field name="approve_state" string="审批状态" widget="badge"
decoration-danger="approve_state == 'end'" decoration-success="approve_state == 'pending'"/>
</tree>
</field>
</record>
@@ -316,6 +316,50 @@ class PropertyReconciliationWizard(models.Model):
line.color = 'success'
def _get_precinct_admin_employees(self, properties):
"""取物业对应的片区管理员(hr.employee):
优先用物业上的「片区管理员」反查员工,取不到时退回物业「管理信息明细」里片区配置的管理员。
只保留配置了企业微信 userid 的员工。
"""
employees = self.env['hr.employee'].sudo()
users = properties.mapped('admin_id')
if users:
employees = employees.search([('user_id', 'in', users.ids)])
if not employees:
employees = properties.mapped('manage_information_ids.precinct_id.employee_id')
return employees.filtered(lambda emp: emp.wecom_userid)
def _notify_registered(self, reconciled_amount):
"""对账变成「挂账」时,给对应片区管理员发企业微信提醒"""
self.ensure_one()
bank = self.bank_id
contracts = self.contract_ids
properties = contracts.mapped('lease_ids.address_id') or bank.property_ids
employees = self._get_precinct_admin_employees(properties)
if not employees:
_logger.info('挂账提醒:银行流水 %s 未找到可通知的片区管理员(物业 %s)', bank.id, properties.ids)
return
lessee_names = '、'.join(sorted(set(contracts.mapped('lessee_id.name')))) or (bank.name_account or '')
contract_codes = '、'.join(sorted(set(contracts.mapped('contract_code')))) or '-'
description = (
'承租方:%s<br/>'
'合同:%s<br/>'
'交易金额:%.2f<br/>'
'对账金额:%.2f<br/>'
'未对金额:%.2f<br/>'
'请核对'
) % (lessee_names, contract_codes, bank.original_amount or 0.0,
reconciled_amount or 0.0, bank.registered or 0.0)
url = 'https://oa.thtzjt.com/web#id=%s&model=property.bank.statements&view_type=form&cids=1' % bank.id
for employee in employees:
try:
self.sudo().env['wecom.apps'].sync_send_message_textcard(
category='oa', user_id=employee.wecom_userid,
title='银行对账挂账提醒', description=description,
url=url, btntxt='查看对账')
except Exception:
_logger.exception('挂账提醒发送失败:银行流水 %s → 员工 %s', bank.id, employee.name)
def confirm(self):
collection_type_mapping = {
'deposit': '押金',
@@ -331,6 +375,7 @@ class PropertyReconciliationWizard(models.Model):
'other': '其他'
}
for rec in self:
was_registered = rec.bank_id.bank_state == 'registered'
bank_lines = self.env['property.bank.line'].search([
('bank', '=', rec.bank_id.name),
('tenant_id.name', '!=', False)
@@ -525,6 +570,10 @@ class PropertyReconciliationWizard(models.Model):
rec.bank_id.yes_amount = rec.bank_id.yes_amount + yes_amount
rec.bank_id.change_amount = sum(line.amount for line in rec.reconciliation_line_ids)
# 本次对账后状态变为「挂账」→ 给对应片区管理员发提醒(原本已是挂账的不重复发)
if rec.bank_id.bank_state == 'registered' and not was_registered:
rec._notify_registered(remaining_amount)
all_contracts = self.contract_ids
remaining_contract_ids = []
for contract in all_contracts: