diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 000000000..9a7e03ede --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 000000000..4ce24f19e --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,27 @@ +{ + "name": "房地产管理系统", # 模块名称 + "summary": "管理房地产属性、优惠和标签", # 模块摘要 + "description": "用于管理房地产属性、报价和标签的模块。", # 模块描述 + "version": "1.0", # 版本号 + "category": "MyAddons/Estate", # 模块分类 + "data": [ + "security/ir.model.access.csv", + "views/estate_property_offer_views.xml", # 视图文件 + "views/estate_property_tag_views.xml", # 视图文件 + "views/estate_property_type_views.xml", # 视图文件 + "views/estate_property_views.xml", # 视图文件 + "views/res_users_views.xml", # 用户视图文件 + "views/report_actions.xml", # 报表操作文件 + "views/report_template.xml", # 报表模板文件 + "views/estate_menus.xml", # 菜单文件 + ], # 数据文件列表 + "assets": { + "web.assets_backend": [ + "estate/static/src/scss/estate.scss", + ] + }, + "depends": ["base", "mail"], # 依赖的模块列表 + "license": "LGPL-3", # 许可证类型 + "installable": True, # 是否可安装 + "application": True, # 是否为应用程序 +} diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 000000000..a9459ed59 --- /dev/null +++ b/estate/models/__init__.py @@ -0,0 +1,5 @@ +from . import estate_property +from . import estate_property_type +from . import estate_property_tag +from . import estate_property_offer +from . import res_users \ No newline at end of file diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 000000000..0bc7e4e7c --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,181 @@ +from odoo import models, fields, api +from datetime import timedelta +from odoo.exceptions import UserError, ValidationError + +# 导入异常类 + + +class EstateProperty(models.Model): + _name = "estate.property" + _description = "房地产属性" + # 继承邮件线程和活动混合类 + _inherit = ["mail.thread", "mail.activity.mixin"] + # 默认按ID降序排列 + _order = "id desc" + + name = fields.Char(string="名称", required=True) + description = fields.Text(string="描述") + postcode = fields.Char(string="邮政编码") + # 默认日期为三个月后 + date_availability = fields.Date( + string="可用日期", + copy=False, + default=lambda self: fields.Date.today() + timedelta(days=90), + ) + expected_price = fields.Float(string="预期价格", required=True) + selling_price = fields.Float(string="销售价格", copy=False) + bedrooms = fields.Integer(string="卧室数量", default=2) + living_area = fields.Integer(string="居住面积") + facades = fields.Integer(string="立面") + garage = fields.Boolean(string="是否有车库") + garden = fields.Boolean(string="是否有花园") + garden_area = fields.Integer(string="花园面积") + garden_orientation = fields.Selection( + [("north", "北"), ("south", "南"), ("east", "东"), ("west", "西")], + string="花园朝向", + ) + # 默认货币为公司货币 + currency_id = fields.Many2one( + "res.currency", + string="货币", + required=True, + default=lambda self: self.env.ref("base.CNY").id, + ) + + active = fields.Boolean(string="是否归档", default=True) + # 房屋类型 + state = fields.Selection( + [ + ("new", "新"), + ("offer_received", "已收到报价"), + ("offer_accepted", "已接受报价"), + ("sold", "已售出"), + ("canceled", "已取消"), + ], + string="状态", + default="new", + required=True, + copy=False, + ) + versionflog = fields.Integer(string="换版", default=1) + # 关联房屋类型模型 + property_type_id = fields.Many2one( + "estate.property.type", string="房屋类型", required=True + ) + # 买家 + buyer_id = fields.Many2one("res.partner", string="买家", copy=False) + # 关联联系人模型卖家 + seller_id = fields.Many2one( + "res.users", string="卖家", required=True, default=lambda self: self.env.user + ) + # 房屋标签 + tag_ids = fields.Many2many("estate.property.tag", string="房屋标签") + # 房屋报价 + offer_ids = fields.One2many( + "estate.property.offer", "property_id", string="房屋报价" + ) + # 房屋总面积 + total_area = fields.Float( + string="总面积", compute="_compute_total_area", store=True + ) + # 最佳报价 + best_price = fields.Float( + string="最佳报价", compute="_compute_best_price", store=True + ) + # 房产预期价格必须严格为正数 + _sql_constraints = [ + ("check_expected_price", "CHECK(expected_price > 0)", "预期价格必须为正数"), + ("check_selling_price", "CHECK(selling_price > 0)", "销售价格必须为正数"), + ("check_bedrooms", "CHECK(bedrooms > 0)", "卧室数量必须为正数"), + ("check_living_area", "CHECK(living_area > 0)", "居住面积必须为正数"), + ("check_facades", "CHECK(facades > 0)", "立面数量必须为正数"), + ] + + # 无法接受低于预期价格 90% 的报价 + @api.constrains("selling_price", "expected_price") # 添加约束 + def _check_selling_price(self): + for record in self: + if record.selling_price < record.expected_price * 0.9: + raise ValidationError("销售价格不能低于预期价格的90%") + + @api.depends("living_area", "garden_area") # 计算字段 + def _compute_total_area(self): + """计算房屋总面积""" + for record in self: + record.total_area = record.living_area + record.garden_area + + @api.depends("offer_ids.price") # 计算字段 + def _compute_best_price(self): + """计算最佳报价""" + for record in self: + best_price = 0.0 + if record.offer_ids: + best_price = max(record.offer_ids.mapped("price")) + record.best_price = best_price + + def action_sold(self): + """标记为已售(需先接受报价)""" + if self.state != "offer_accepted": + raise UserError("必须接受报价后才能标记已售") + return self.write({"state": "sold"}) + + def action_cancel(self): + """取消交易""" + if self.state == "sold": + raise UserError("已售房产不可取消") + return self.write({"state": "canceled"}) + + # 如果有花园,设置默认花园面积为50,朝向为南,若没有花园,则设置花园面积为0,朝向为无 + @api.onchange("garden") + def _onchange_garden(self): + if self.garden: + self.garden_area = 50 + self.garden_orientation = "south" + else: + self.garden_area = 0 + self.garden_orientation = False + + # 若取消选择车库则弹出警告 + @api.onchange("garage") + def _onchange_garage(self): + if not self.garage == False: + return {"warning": {"title": "警告", "message": "确定取消车库吗?"}} + + # •如果房产的状态不是‘新建’或‘已取消’,则阻止删除房产 + @api.ondelete(at_uninstall=True) + def _unlink_except_new_canceled(self): + """删除房产""" + for record in self: + if record.state not in ["new", "canceled"]: + raise UserError("只能删除新建或已取消的房产") + + # 设置版本日志 + def setversionflog(self): + # 空函数,不做任何操作 + pass + + def write(self, vals): + vals["create_uid"] = self.create_uid + return super(EstateProperty, self).write(vals) + + # 定义按钮 + def action_set_offer_received(self): + """设置为已收到报价""" + return self.write({"state": "offer_received"}) + + def action_set_offer_accepted(self): + """设置为已接受报价""" + return self.write({"state": "offer_accepted"}) + def action_set_sold(self): + """设置为已售出""" + # 将状态设置为已售出 + return self.write({"state": "sold"}) + + def do_reject_new(self): + """拒绝新建报价""" + # 将状态设置为"new" + return self.write({"state": "new"}) + + def do_reject_offer(self): + """拒绝报价""" + return self.write({"state": "offer_received"}) diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 000000000..a75dcc724 --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,77 @@ +from odoo import models,fields,api;# 导入Odoo模型、字段和API模块 +from datetime import timedelta;# 导入时间模块 +from odoo.exceptions import UserError;# 导入用户错误模块 +from odoo.exceptions import ValidationError;# 导入验证错误模块 +# 房屋报价模型 +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer'; + _description = '房屋报价' + _order = "price desc";# 默认按价格降序排列 + + price = fields.Float(string='报价', required=True); + status = fields.Selection([('Accepted', '已接受'),('Refused', '已拒绝')], string='状态',copy=False); + partner_id = fields.Many2one('res.partner', string='客户', required=True); + property_id = fields.Many2one('estate.property', string='房屋', required=True); + validity = fields.Integer(string='有效期', default=7);# 默认有效期为7天 + date_deadline = fields.Date(string='截止日期', compute='_compute_date_deadline',inverse='_inverse_validity', store=True);# 计算字段,存储在数据库中 + property_type_id = fields.Many2one(related='property_id.property_type_id', string='房屋类型', store=True);# 关联房屋类型模型 + + + # 保价必须为严格正数 + _sql_constraints = [ + ('check_price', 'CHECK(price > 0)', '报价必须为正数') + ] + @api.depends('validity',"create_date")# 计算字段 + def _compute_date_deadline(self): + for record in self: + if record.create_date: + # 计算截止日期 + record.date_deadline = record.create_date + timedelta(days=record.validity); + else: + # 如果创建日期为空,则为当前日期 + record.date_deadline = fields.Date.today()+ timedelta(days=record.validity); + # 反向计算有效期,inverse方法 + def _inverse_validity(self): + for record in self: + if record.date_deadline: + if record.create_date: + validity = (record.date_deadline - record.create_date.date()).days; + # 如果有效期小于0,则抛出异常 + if validity < 0: + raise UserError("有效期不能小于0天") + record.validity = validity; + else: + validity = (record.date_deadline - fields.Datetime.now().date()).days; + # 如果有效期小于0,则抛出异常 + if validity < 0: + raise UserError("有效期不能小于0天") + record.validity = validity; + # 接受报价 + def action_accept(self): + # 将房屋状态改为已接受报价 + self.property_id.state = 'offer_accepted'; + # 将房屋的销售价格设置为报价价格 + self.property_id.selling_price = self.price; + # 将买家设置为报价的客户 + self.property_id.buyer_id = self.partner_id.id; + # 将报价的状态设置为已接受 + self.status = 'Accepted'; + # 拒绝报价 + def action_refuse(self): + # 将房屋状态改为已收到报价 + self.property_id.state = 'offer_received'; + # 将报价的状态设置为已拒绝 + self.status = 'Refused'; + + # 创建报价时,将房产状态设置为‘收到报价’。如果用户尝试创建价格低于现有报价的报价,则引发错误。 + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + # 检查房产状态是否为‘收到报价’ + property_id = vals.get('property_id') + property_record = self.env['estate.property'].browse(property_id); + property_record.state = 'offer_received'; + price = vals.get('price') + if price + + + \ No newline at end of file diff --git a/estate/static/src/scss/estate.scss b/estate/static/src/scss/estate.scss new file mode 100644 index 000000000..33f322628 --- /dev/null +++ b/estate/static/src/scss/estate.scss @@ -0,0 +1,20 @@ +.estate_form .o_box.stat-card { + background: #fff; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.2s ease-in-out; +} + +.estate_form .o_box.stat-card:hover { + transform: translateY(-3px); +} + +.estate_form .o_box.stat-card h3 { + font-size: 18px; + font-weight: bold; + margin: 5px 0 0 0; +} + +.estate_form .o_box.stat-card span { + font-size: 14px; + color: #888; +} \ No newline at end of file diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 000000000..0b9991309 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 000000000..f6c0222bd --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,38 @@ + + + 房屋报价 + estate.property.offer + tree + [('property_type_id', '=', active_id)] + + + + estate.property.offer.tree + estate.property.offer + + + + + + + + + + + + +

+ +

+
+ +
+ + + + + + + + + + + + + + + +
+ +
+
+ + + estate.property.type.search + estate.property.type + + + + + + +
\ No newline at end of file diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 000000000..cd40a0050 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,165 @@ + + + 房地产信息 + estate.property + tree,form,kanban + {} + + + + estate.property.tree + estate.property + + + + + + + + + + + + + + + + estate.property.form.custom + estate.property + +
+
+ + + + + +
+ + +
+

+ +

+
+
+ +
+

📑 房屋报价

+ +
+
+ +
+ +
+

📅 基础信息

+ + + + + + + + + + +
+ +
+

💰 费用信息

+ + + + + + + + + +
+
+ 总面积 (㎡) +

+ +

+
+
+ 销售价格 +

+ +

+
+
+ 预期价格 +

+ +

+
+
+ 最佳报价 +

+ +

+
+
+
+
+
+
+
+
+ + + estate.property.search + estate.property + + + + + + + + + + + + + + + + + + + + estate.property + + + + + + +
+
+ + + +
预期价格: + + +
+
+ +
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/estate/views/report_actions.xml b/estate/views/report_actions.xml new file mode 100644 index 000000000..bec00b288 --- /dev/null +++ b/estate/views/report_actions.xml @@ -0,0 +1,12 @@ + + + 房产详情报告 + estate.property + + qweb-pdf + estate.report_property_template + '房产报告-%s' % object.name + + report + + \ No newline at end of file diff --git a/estate/views/report_template.xml b/estate/views/report_template.xml new file mode 100644 index 000000000..03a980152 --- /dev/null +++ b/estate/views/report_template.xml @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 000000000..f1928d5c7 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,14 @@ + + + res.users.view.form.estate + res.users + + + + + + + + + + \ No newline at end of file