diff --git a/dental/__init__.py b/dental/__init__.py new file mode 100644 index 0000000000..5607426d8a --- /dev/null +++ b/dental/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controller diff --git a/dental/__manifest__.py b/dental/__manifest__.py new file mode 100644 index 0000000000..8350d97992 --- /dev/null +++ b/dental/__manifest__.py @@ -0,0 +1,23 @@ +{ + 'name': 'dental', + 'version': '1.0', + 'summary': 'dental management system', + 'description': 'A module for managing dental problems and related entities.', + 'category': 'website', + 'author': 'YASP', + 'sequence': 1, + 'depends': ['base', 'website', 'mail', 'account'], + 'license': 'LGPL-3', + 'data': [ + 'security/ir.model.access.csv', + 'views/dental_portal_templates.xml', + 'views/dental_views.xml', + 'views/medical_aids_views.xml', + 'views/medical_symptoms_views.xml', + 'views/medication_views.xml', + 'views/dental_patient_history_views.xml', + 'views/dental_menu.xml', + ], + 'installable': True, + 'application': True, +} diff --git a/dental/controller/__init__.py b/dental/controller/__init__.py new file mode 100644 index 0000000000..21cf4bce06 --- /dev/null +++ b/dental/controller/__init__.py @@ -0,0 +1 @@ +from . import dental_portal diff --git a/dental/controller/dental_portal.py b/dental/controller/dental_portal.py new file mode 100644 index 0000000000..8190341855 --- /dev/null +++ b/dental/controller/dental_portal.py @@ -0,0 +1,55 @@ +from odoo import http +from odoo.http import request +from odoo.addons.portal.controllers.portal import CustomerPortal + + +class DentalPortal(CustomerPortal): + @http.route('/my/dental', type='http', auth='user', website=True) + def portal_my_dental(self, **kw): + patients = request.env['dental.patient'].search([('guarantor_id', '=', request.env.user.id), ('gender', '=', 'male'), ('patient_history_ids', '!=', False), ('emergency_contact_id', '!=', False)]) + return request.render('dental.portal_my_dental', { + 'patients': patients + }) + + @http.route('/my/dental/', type='http', auth='user', website=True) + def portal_my_dental_user(self, patient_id, **kw): + patient = request.env['dental.patient'].browse(patient_id) + return request.render('dental.portal_my_dental_user', { + 'patient': patient + }) + + @http.route('/my/dental//personal', type='http', auth='user', website=True) + def portal_my_dental_personal(self, patient_id, **kw): + patient = request.env['dental.patient'].browse(patient_id) + return request.render('dental.portal_my_dental_personal', { + 'patient': patient + }) + + @http.route('/my/dental//medical_history', type='http', auth='user', website=True) + def portal_my_dental_medical_history(self, patient_id, **kw): + history = request.env['dental.patient.history'].search([('patient_id', '=', patient_id)]) + return request.render('dental.portal_my_dental_medical_history', { + 'history': history + }) + + @http.route('/my/dental//medical_aid', type='http', auth='user', website=True) + def portal_my_dental_medical_aid(self, patient_id, **kw): + patient = request.env['dental.patient'].browse([patient_id]) + return request.render('dental.portal_my_dental_medical_aid', { + 'patient': patient + }) + + @http.route('/my/dental//dental_history', type='http', auth='user', website=True) + def portal_my_dental_dental_history(self, patient_id, **kw): + history = request.env['dental.patient.history'].search([('patient_id', '=', patient_id)]) + return request.render('dental.portal_my_dental_dental_history', { + 'history': history, + 'patient_id': patient_id + }) + + @http.route('/my/dental//dental_history/', type='http', auth='user', website=True) + def portal_my_dental_dental_history_details(self, patient_id, history_id, **kw): + history_record = request.env['dental.patient.history'].browse(history_id) + return request.render('dental.portal_my_dental_dental_history_details', { + 'history_record': history_record + }) diff --git a/dental/models/__init__.py b/dental/models/__init__.py new file mode 100644 index 0000000000..78c51e0eb5 --- /dev/null +++ b/dental/models/__init__.py @@ -0,0 +1,5 @@ +from . import dental_patients +from . import medical_aids +from . import medical_symptoms +from . import medication +from . import dental_patient_history diff --git a/dental/models/dental_patient_history.py b/dental/models/dental_patient_history.py new file mode 100644 index 0000000000..554d2b3832 --- /dev/null +++ b/dental/models/dental_patient_history.py @@ -0,0 +1,104 @@ +from odoo import models, fields, api +from datetime import date + + +class DentalPatientHistory(models.Model): + _name = 'dental.patient.history' + _description = 'Dental Patient History' + + # Basic information + name = fields.Char(string="Title", required=True) + description = fields.Text(string="Description") + patient_id = fields.Many2one('dental.patient', string="Patient", required=True) + date = fields.Datetime(string="Date", default=fields.Datetime.now, required=True) + main_complaint = fields.Text(string="Main Complaint") + history = fields.Text(string="History") + company_id = fields.Many2one('res.company', string='Company') + did_not_attend = fields.Boolean(string="Did Not Attend") + tags = fields.Char(string="Tags") + + # X-ray file uploads + xray_file_1 = fields.Binary(string="X-ray File 1") + xray_file_2 = fields.Binary(string="X-ray File 2") + clear_aligner_file_1 = fields.Binary(string="Clear Aligner File 1") + clear_aligner_file_2 = fields.Binary(string="Clear Aligner File 2") + + # Other details + habits = fields.Text(string="Habits") + extra_oral_observation = fields.Text(string="Extra-Oral Observation") + + # Tooth chart (for example purposes) + teeth_chart = fields.Char(string="Teeth Chart") + + # Treatment notes + treatment_notes = fields.Text(string="Treatment Notes") + + # Billing information + consultation_type = fields.Selection([ + ('full_consultation', 'Full Consultation with Bitewings and Scan'), + ('basic_consultation', 'Basic Consultation'), + ('no_consultation', 'No Consultation') + ], string="Consultation Type") + + call_out = fields.Boolean(string="Call Out") + scale_and_polish = fields.Boolean(string="Scale and Polish") + fluoride = fields.Boolean(string="Fluoride") + + filling_description = fields.Text(string="Filling Description") + + aligner_delivery = fields.Boolean( + string="Aligner Delivery and Attachment Placed") + whitening = fields.Boolean(string="Whitening") + + fissure_sealant_qty = fields.Float( + string="Fissure Sealant Quantity", digits=(6, 2)) + + attachments_removed = fields.Boolean(string="Attachments Removed") + aligner_followup_scan = fields.Boolean(string="Aligner Follow-up Scan") + + other_notes = fields.Text(string="Other Notes") + upper_18_staining = fields.Boolean(string='18 Staining') + upper_17_staining = fields.Boolean(string='17 Staining') + upper_16_staining = fields.Boolean(string='16 Staining') + upper_15_staining = fields.Boolean(string='15 Staining') + upper_14_staining = fields.Boolean(string='14 Staining') + upper_13_staining = fields.Boolean(string='13 Staining') + upper_12_staining = fields.Boolean(string='12 Staining') + upper_11_staining = fields.Boolean(string='11 Staining') + upper_28_staining = fields.Boolean(string='28 Staining') + upper_27_staining = fields.Boolean(string='27 Staining') + upper_26_staining = fields.Boolean(string='26 Staining') + upper_25_staining = fields.Boolean(string='25 Staining') + upper_24_staining = fields.Boolean(string='24 Staining') + upper_23_staining = fields.Boolean(string='23 Staining') + upper_22_staining = fields.Boolean(string='22 Staining') + upper_21_staining = fields.Boolean(string='21 Staining') + lower_31_staining = fields.Boolean(string='31 Staining') + lower_32_staining = fields.Boolean(string='32 Staining') + lower_33_staining = fields.Boolean(string='33 Staining') + lower_34_staining = fields.Boolean(string='34 Staining') + lower_35_staining = fields.Boolean(string='35 Staining') + lower_36_staining = fields.Boolean(string='36 Staining') + lower_37_staining = fields.Boolean(string='37 Staining') + lower_38_staining = fields.Boolean(string='38 Staining') + lower_41_staining = fields.Boolean(string='41 Staining') + lower_42_staining = fields.Boolean(string='42 Staining') + lower_43_staining = fields.Boolean(string='43 Staining') + lower_44_staining = fields.Boolean(string='44 Staining') + lower_45_staining = fields.Boolean(string='45 Staining') + lower_46_staining = fields.Boolean(string='46 Staining') + lower_47_staining = fields.Boolean(string='47 Staining') + lower_48_staining = fields.Boolean(string='48 Staining') + + # General notes field at the end + notes = fields.Text(string="Additional Notes") + + @api.depends("patient_id") + def _compute_name(self): + for record in self: + record.name = f"{record.patient_id.name}-{date.today()}" + + def action_save_close(self): + # Method to save and close the form view + self.ensure_one() + self.env['ir.actions.act_window'].browse(self._context.get('active_id')).write({'state': 'done'}) diff --git a/dental/models/dental_patients.py b/dental/models/dental_patients.py new file mode 100644 index 0000000000..4c01c6d49a --- /dev/null +++ b/dental/models/dental_patients.py @@ -0,0 +1,96 @@ +from odoo import models, fields, Command + + +class DentalPatients(models.Model): + _name = "dental.patient" + _description = "Patients" + _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] + + name = fields.Char(string='Name', required=True) + state = fields.Selection([ + ('new', 'New'), + ('todotoday', 'To Do Today'), + ('done', 'Done'), + ('toinvoice', 'To Invoice') + ], default='new', tracking=True) + + gp_id = fields.Many2one('res.partner', string="GP's Name") + gp_phone = fields.Char(related='gp_id.phone', string="GP's Phone", readonly=True) + chronic_conditions = fields.Many2many('chronic.conditions', string="Chronic Conditions") + medication = fields.Many2many('dental.medication', string="Medication") + hospitalized_this_year = fields.Boolean(string="Hospitalised this Year") + allergies = fields.Many2many('dental.allergies', string="Allergies") + habits_substance_abuse = fields.Many2many('habits.substance', string="Habits and Substance Abuse") + under_specialist_care = fields.Char(string="Under Specialist Care") + psychiatric_history = fields.Char(string="Psychiatric History") + image_1920 = fields.Image(string="Image") + gender = fields.Selection([ + ('female', 'Female'), + ('male', 'Male'), + ('neither', 'Neither') + ], string="Gender") + is_pregnant = fields.Boolean(string="Are you pregnant?", default=False, help="Visible if gender is Female") + is_nursing = fields.Boolean(string="Are you nursing?", default=False, help="Visible if gender is Female") + hormone_treatment = fields.Selection([ + ('hormone', 'Hormone Replacement Treatment'), + ('birth_control', 'Birth control'), + ('neither', 'Neither') + ], string="Are you on...", default='neither', help="Visible if gender is Female") + + medical_aid_id = fields.Many2one('medical.aids', string="Medical Aid") + medical_aid_plan = fields.Char(string="Medical Aid Plan") + medical_aid_number = fields.Char(string="Medical Aid Number") + main_number_code = fields.Char(string="Main Number Code") + dependant_code = fields.Char(string="Dependant Code") + + # Fields for Patient Details + emergency_contact_id = fields.Many2one('res.partner', string="Emergency Contact") + mobile = fields.Char(related='emergency_contact_id.phone', string="Mobile", readonly=True) + company_id = fields.Many2one('res.company', string="Company/School") + occupation = fields.Char(string="Occupation/Grade") + identity_number = fields.Char(string="Identity Number") + date_of_birth = fields.Date(string="Date of Birth") + marital_status = fields.Selection([ + ('single', 'Single'), + ('married', 'Married'), + ('divorced', 'Divorced'), + ('widowed', 'Widowed') + ], string="Marital Status") + + # Fields for Consent Form + consent_signature = fields.Binary(string="Consent Signature") + consent_date = fields.Date(string="Consent Date") + + guarantor_id = fields.Many2one('res.users', string="Guarantor") + guarantor_mobile = fields.Char(related='guarantor_id.mobile', string="Guarantor Mobile Phone", readonly=False) + guarantor_phone = fields.Char(related='guarantor_id.phone', string="Phone", readonly=True) + guarantor_email = fields.Char(related='guarantor_id.email', string="Email", readonly=True) + + # Add this field to link history records + patient_history_ids = fields.One2many('dental.patient.history', 'patient_id', string="Patient History") + + def action_invoice(self): + self.state = 'toinvoice' + move_vals = { + 'partner_id': self.guarantor_id.id, + 'move_type': 'out_invoice', + 'invoice_date': fields.Date.today(), + "invoice_line_ids": [ + Command.create({ + "name": self.name, + "quantity": 1, + "price_unit": 100 + }), + ], + + } + self.env['account.move'].create(move_vals) + + def book_appointment_button(self): + vals = { + 'name': f"{self.name}-Dentist Booking", + 'appointment_type_id': self.env.ref('appointment.appointment_type_dental_care').id, + 'duration': 0.5 + } + self.env['calendar.event'].create(vals) + self.state = 'toinvoice' diff --git a/dental/models/medical_aids.py b/dental/models/medical_aids.py new file mode 100644 index 0000000000..bd1fcdc635 --- /dev/null +++ b/dental/models/medical_aids.py @@ -0,0 +1,21 @@ +from odoo import models, fields + + +class MedicalAids(models.Model): + _name = "medical.aids" + _description = "Medical Insurance Info" + + name = fields.Char(string='Name', required=True) + state = fields.Selection([ + ('new', 'New'), + ('inprogress', 'In Progress'), + ('done', 'Done')], + string='Status', + default='new' + ) + contact = fields.Many2one('res.partner', string='Contact', required=True) + phone = fields.Char(related='contact.phone', string='Phone', readonly=True) + email = fields.Char(related='contact.email', string='Email', readonly=True) + company_id = fields.Many2one('res.company', string='Company', required=True) + note = fields.Text(string='Notes') + image = fields.Binary(string='Image') diff --git a/dental/models/medical_symptoms.py b/dental/models/medical_symptoms.py new file mode 100644 index 0000000000..ae2f4780a5 --- /dev/null +++ b/dental/models/medical_symptoms.py @@ -0,0 +1,27 @@ +from odoo import models, fields + + +class ChronicConditions(models.Model): + _name = "chronic.conditions" + _description = "Chronic medical conditions" + _order = 'sequence, name' + + name = fields.Char(string='Name', required=True) + sequence = fields.Integer(string='Sequence') + parent_id = fields.Many2one('chronic.conditions', string='Parent Condition', ondelete='cascade', index=True) + + +class Allergies(models.Model): + _name = "dental.allergies" + _description = "Allergies" + + name = fields.Char(string='Name', required=True) + parent_id = fields.Many2one('chronic.conditions', string='Parent Condition', ondelete='cascade', index=True) + + +class HabitsSubstance(models.Model): + _name = "habits.substance" + _description = "Habits and substance abuse information" + + name = fields.Char(string='Name', required=True) + parent_id = fields.Many2one('chronic.conditions', string='Parent Condition', ondelete='cascade', index=True) diff --git a/dental/models/medication.py b/dental/models/medication.py new file mode 100644 index 0000000000..b2881b4d19 --- /dev/null +++ b/dental/models/medication.py @@ -0,0 +1,10 @@ +from odoo import models, fields + + +class Medication(models.Model): + _name = "dental.medication" + _description = "Medicinal Info" + _order = "sequence, name" + + name = fields.Char(string='Name', required=True) + sequence = fields.Integer() diff --git a/dental/security/ir.model.access.csv b/dental/security/ir.model.access.csv new file mode 100644 index 0000000000..295eefefb9 --- /dev/null +++ b/dental/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_dental_patient,access_dental_patient,dental.model_dental_patient,base.group_user,1,1,1,1 +access_medical_aids,access_medical_aids,dental.model_medical_aids,base.group_user,1,1,1,1 +access_chronic_conditions,access_chronic_conditions,dental.model_chronic_conditions,base.group_user,1,1,1,1 +access_dental_allergies,access_dental_allergies,dental.model_dental_allergies,base.group_user,1,1,1,1 +access_habits_substance,access_habits_substance,dental.model_habits_substance,base.group_user,1,1,1,1 +access_dental_medication,access_dental_medication,dental.model_dental_medication,base.group_user,1,1,1,1 +access_dental_patient_history,access_dental_patient_history,dental.model_dental_patient_history,base.group_user,1,1,1,1 diff --git a/dental/static/src/image/dental.png b/dental/static/src/image/dental.png new file mode 100644 index 0000000000..3bec818cc8 Binary files /dev/null and b/dental/static/src/image/dental.png differ diff --git a/dental/static/src/image/dental.svg b/dental/static/src/image/dental.svg new file mode 100644 index 0000000000..6fdfaaffce --- /dev/null +++ b/dental/static/src/image/dental.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/dental/views/dental_menu.xml b/dental/views/dental_menu.xml new file mode 100644 index 0000000000..80fc48701f --- /dev/null +++ b/dental/views/dental_menu.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + diff --git a/dental/views/dental_patient_history_views.xml b/dental/views/dental_patient_history_views.xml new file mode 100644 index 0000000000..ecd28ce53f --- /dev/null +++ b/dental/views/dental_patient_history_views.xml @@ -0,0 +1,156 @@ + + + Dental Patient History Form + dental.patient.history + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+
18
+
17
+
16
+
15
+
14
+
13
+
12
+
11
+
28
+
27
+
26
+
25
+
24
+
23
+
22
+
21
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
48
+
47
+
46
+
45
+
44
+
43
+
42
+
41
+
38
+
37
+
36
+
35
+
34
+
33
+
32
+
31
+
+
+
+ + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + + + + + + + + + + + + +
+ +
+
+
+
+
+ + dental.patient.history.tree + dental.patient.history + + + + + + + + + + + Dental Patient History + dental.patient.history + tree,form + +
diff --git a/dental/views/dental_portal_templates.xml b/dental/views/dental_portal_templates.xml new file mode 100644 index 0000000000..67ce445f94 --- /dev/null +++ b/dental/views/dental_portal_templates.xml @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + diff --git a/dental/views/dental_views.xml b/dental/views/dental_views.xml new file mode 100644 index 0000000000..7e33e23c10 --- /dev/null +++ b/dental/views/dental_views.xml @@ -0,0 +1,163 @@ + + + + Patients + dental.patient + kanban,tree,form + +

+ Register New Patients +

+
+
+ + dental.patient.kanban + dental.patient + + + + + +
+
+ + + +
+ +
+
+
+
+
+
+
+
+ + + dental.patient.tree + dental.patient + + + + + + + + + dental.patient.form + dental.patient + +
+
+ +
+ + +

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+
diff --git a/dental/views/medical_aids_views.xml b/dental/views/medical_aids_views.xml new file mode 100644 index 0000000000..b9bc482f4d --- /dev/null +++ b/dental/views/medical_aids_views.xml @@ -0,0 +1,61 @@ + + + + + Medical Aids + medical.aids + tree,form + +

+ Create New Medical Aid Record +

+
+
+ + + + medical.aids.tree + medical.aids + + + + + + + + + + + + medical.aids.form + medical.aids + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+
+
+
diff --git a/dental/views/medical_symptoms_views.xml b/dental/views/medical_symptoms_views.xml new file mode 100644 index 0000000000..b1f7f9ec48 --- /dev/null +++ b/dental/views/medical_symptoms_views.xml @@ -0,0 +1,119 @@ + + + + Chronic Conditions + chronic.conditions + tree,form + +

+ Create New Condition +

+
+
+ + + chronic.conditions.tree + chronic.conditions + + + + + + + + + + chronic.conditions.form + chronic.conditions + +
+ + + + + + + + +
+
+
+ + + Allergies + dental.allergies + tree,form + +

+ Create New Allergy +

+
+
+ + + dental.allergies.tree + dental.allergies + + + + + + + + + + dental.allergies.form + dental.allergies + +
+ + + + + + + + +
+
+
+ + + Habits/Substance Abuse + habits.substance + tree,form + +

+ Create New Habit or Substance +

+
+
+ + + habits.substance.tree + habits.substance + + + + + + + + + + habits.substance.form + habits.substance + +
+ + + + + + + + +
+
+
+
diff --git a/dental/views/medication_views.xml b/dental/views/medication_views.xml new file mode 100644 index 0000000000..4f3a24cd31 --- /dev/null +++ b/dental/views/medication_views.xml @@ -0,0 +1,33 @@ + + + + Medication + dental.medication + tree,form + + + + medication.tree + dental.medication + + + + + + + + + + medication.form + dental.medication + +
+ + + + + +
+
+
+
diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 0000000000..cc9ec6bcb3 --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,4 @@ +from . import models +from . import wizard +from . import report +from . import controller diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 0000000000..74a0fc55da --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,34 @@ +{ + 'name': 'estate', + 'version': '1.0', + 'summary': 'A real estate management application', + 'description': 'A module for managing real estate properties, offers, and related entities.', + 'category': 'Real Estate/Brokerage', + 'author': 'YASP', + 'website': 'https://www.yaspestate.com', + 'sequence': 1, + 'depends': ['base', 'website'], + 'license': 'LGPL-3', + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'data/data.xml', + 'report/estate_property_offer_template.xml', + 'report/estate_property_template.xml', + 'report/estate_property_report.xml', + 'wizard/estate_add_offer_wizard_views.xml', + 'data/estate_property_type_data.xml', + 'views/estate_property_views.xml', + 'views/estate_property_offers_views.xml', + 'views/estate_property_types_views.xml', + 'views/estate_property_tags_views.xml', + 'views/estate_property_website_templates_views.xml', + 'views/res_users_views.xml', + 'views/estate_menus.xml' + ], + 'demo': [ + 'demo/estate_property_demo.xml' + ], + 'installable': True, + 'application': True, +} diff --git a/estate/controller/__init__.py b/estate/controller/__init__.py new file mode 100644 index 0000000000..de87d02b2d --- /dev/null +++ b/estate/controller/__init__.py @@ -0,0 +1,2 @@ + +from . import available_properties diff --git a/estate/controller/available_properties.py b/estate/controller/available_properties.py new file mode 100644 index 0000000000..a72f4cf68c --- /dev/null +++ b/estate/controller/available_properties.py @@ -0,0 +1,27 @@ +from odoo import http +from odoo.http import request + + +class PropertyController(http.Controller): + + @http.route(['/properties', '/properties/page/'], auth='public', website=True) + def terms_conditions(self, page=1, **kwargs): + domain = [('state', 'not in', ['sold', 'cancelled'])] + total_properties = request.env["estate.property"].search_count(domain) + pager = request.website.pager( + url='/properties', + total=total_properties, + page=page, + step=6, + ) + properties = request.env['estate.property'].search(domain, limit=6, offset=pager['offset']) + return request.render('estate.available_properties', { + 'properties': properties, + 'pager': pager, + }) + + @http.route('/property/', auth='public', website=True) + def property(self, property_avail, **kwargs): + return request.render('estate.property_template', { + 'property': property_avail + }) diff --git a/estate/data/data.xml b/estate/data/data.xml new file mode 100644 index 0000000000..886692abcc --- /dev/null +++ b/estate/data/data.xml @@ -0,0 +1,11 @@ + + + + + Properties + /properties + + 100 + + + diff --git a/estate/data/estate_property_type_data.xml b/estate/data/estate_property_type_data.xml new file mode 100644 index 0000000000..cda16b0808 --- /dev/null +++ b/estate/data/estate_property_type_data.xml @@ -0,0 +1,18 @@ + + + Residential + 10 + + + Commercial + 20 + + + Industrial + 30 + + + Land + 40 + + diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml new file mode 100644 index 0000000000..a620bd3347 --- /dev/null +++ b/estate/demo/estate_property_demo.xml @@ -0,0 +1,95 @@ + + + + Big Villa + + new + A nice and big villa + 12345 + 2020-02-02 + 1600000 + 1440000 + 6 + 100 + 4 + True + True + 100000 + south + + + + Trailer home + + canceled + Home in a trailer park + 54321 + 1970-01-01 + 100000 + 90000 + 1 + 10 + 4 + False + True + 500 + north + + + + Modern Apartment + + new + A modern apartment in the city center + 67890 + 2024-01-01 + 500000 + 480000 + 3 + 80 + 3 + True + False + 0 + north + + + + + + 10000 + + + + + + + 1500000 + + + + + + + 1500001 + + + + + + + + + + diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 0000000000..9a2189b638 --- /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 diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 0000000000..e634551a51 --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,121 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError +from datetime import timedelta +from odoo.tools.float_utils import float_compare + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Real Estate Property' + _order = 'id desc' + + name = fields.Char(string='Name', required=True) + description = fields.Text(string='Description') + postcode = fields.Char(string='Postcode') + date_availability = fields.Date( + string='Availability Date', + copy=False, + default=fields.Datetime.today() + timedelta(days=90) + ) + expected_price = fields.Float(string='Expected Price', required=True) + selling_price = fields.Float(string='Selling Price', readonly=True, copy=False) + bedrooms = fields.Integer(string='Bedrooms', default=2) + living_area = fields.Integer(string='Living Area (sqm)') + facades = fields.Integer(string='Facades') + garage = fields.Boolean(string='Garage') + garden = fields.Boolean(string='Garden') + garden_area = fields.Integer(string='Garden Area (sqm)') + garden_orientation = fields.Selection([ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], string='Garden Orientation') + active = fields.Boolean(string='Active', default=True) + state = fields.Selection( + string='State', + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('canceled', 'Canceled') + ], + required=True, + copy=False, + default='new', + readonly=True + ) + property_type_id = fields.Many2one('estate.property.type', string='Property Type') + buyer_id = fields.Many2one('res.partner', string='Buyer') + seller_id = fields.Many2one('res.users', string='Salesman', default=lambda self: self.env.user) + tag_ids = fields.Many2many('estate.property.tag', string='Tags') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + total_area = fields.Float(compute='_compute_total_area', store=True) + best_price = fields.Float(compute='_compute_best_price', store=True) + company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company.id) + + @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: + if record.offer_ids: + record.best_price = max(record.offer_ids.mapped('price')) + else: + record.best_price = 0.0 + + @api.onchange('garden') + def _onchange_garden(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = False + + def action_sold(self): + for record in self: + if record.state == 'canceled': + raise ValidationError("Canceled properties cannot be set as sold.") + record.state = 'sold' + return True + + def action_cancel(self): + for record in self: + if record.state == 'sold': + raise ValidationError("Sold properties cannot be canceled.") + record.state = 'canceled' + return True + + def unlink(self): + for record in self: + if record.state in ['sold', 'canceled']: + raise ValidationError("You cannot delete a property that is sold or canceled.") + related_offers = self.env['estate.property.offer'].search([('property_id', '=', record.id)]) + if related_offers: + related_offers.unlink() + return super().unlink() + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if record.expected_price > 0: + if record.selling_price and float_compare(record.selling_price, record.expected_price * 0.9, precision_rounding=0.01) < 0: + raise ValidationError("The selling price must be at least 90% of the expected price.") + + @api.model + def ondelete(self): + """Prevent deletion of properties that are not in 'New' or 'Canceled' state.""" + for record in self: + if record.state not in ['new', 'canceled']: + raise ValidationError("You cannot delete a property that is not in the 'New' or 'Canceled' state.") + return super().unlink() + + _sql_constraints = [ + ('check_expected_price', 'CHECK(expected_price > 0)', 'The expected price must be strictly positive.'), + ('check_selling_price', 'CHECK(selling_price >= 0)', 'The selling price must be positive.') + ] diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 0000000000..cdcf6aa1fe --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,98 @@ +from odoo import models, fields, api +from odoo.exceptions import ValidationError +from datetime import timedelta + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Estate Property Offer' + _order = 'price desc' + + price = fields.Float(string='Price', required=True) + status = fields.Selection([ + ('accepted', 'Accepted'), + ('refused', 'Refused'), + ('draft', 'Draft') + ], default='draft', copy=False) + # state = fields.Selection([ + # ('accepted', 'Accepted'), + # ('refused', 'Refused'), + # ('draft', 'Draft') + # ], default='draft', copy=False) + partner_id = fields.Many2one('res.partner', string='Partner', required=True) + property_id = fields.Many2one('estate.property', string='Property', required=True, ondelete='cascade') + property_type_id = fields.Many2one( + related='property_id.property_type_id', + store=True, + string='Property Type' + ) + validity = fields.Integer(string='Validity (days)', default=7) + date_deadline = fields.Date(string='Deadline', compute="_compute_date_deadline", inverse="_inverse_date_deadline", store=True) + + @api.depends("create_date", "validity") + 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) + + @api.depends("date_deadline") + def _inverse_date_deadline(self): + for record in self: + if record.create_date: + record.validity = (record.date_deadline - record.create_date.date()).days + else: + record.validity = (record.date_deadline - fields.Date.today()).days + + def action_accept(self): + for offer in self: + if offer.status == 'accepted': + raise ValidationError("This offer is already accepted.") + if offer.status == 'refused': + raise ValidationError("This offer is refused and cannot be accepted.") + other_accepted_offers = self.search([ + ('property_id', '=', offer.property_id.id), + ('status', '=', 'accepted') + ]) + if other_accepted_offers: + raise ValidationError("An offer for this property has already been accepted.") + offer.status = 'accepted' + offer.property_id.state = 'offer_accepted' + offer.property_id.selling_price = offer.price + offer.property_id.buyer_id = offer.partner_id + other_offers = self.search([ + ('property_id', '=', offer.property_id.id), + ('id', '!=', offer.id) + ]) + other_offers.write({'status': 'refused'}) + return True + + def action_refuse(self): + for offer in self: + if offer.status == 'refused': + raise ValidationError("This offer is already refused.") + if offer.status == 'accepted': + raise ValidationError("An accepted offer cannot be refused.") + offer.status = 'refused' + return True + + @api.model + def create(self, vals): + + property_id = self.env['estate.property'].browse(vals['property_id']) + if property_id.best_price and vals['price'] <= property_id.best_price: + raise ValidationError("The offer price must be higher than any existing offer.") + offer = super().create(vals) + offer.property_id.state = 'offer_received' + return offer + + def action_confirm_offer(self): + for offer in self: + if offer.property_id: + offer.property_id.write({'state': 'offer_received'}) + self.write({'status': 'accepted'}) + + _sql_constraints = [ + ('check_offer_price', 'CHECK(price > 0)', 'The offer price must be strictly positive.') + ] diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 0000000000..94beede386 --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,14 @@ +from odoo import models, fields + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Property Tag' + _order = 'name asc' + + name = fields.Char('Name', required=True) + color = fields.Integer(string="Color") + + _sql_constraints = [ + ('unique_tag_name', 'UNIQUE(name)', 'The tag name must be unique.') + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 0000000000..3c5b26fd67 --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,39 @@ +from odoo import models, fields, api + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Property Type' + _order = "sequence, name asc" + + sequence = fields.Integer(string="Sequence", default=10) + name = fields.Char('Name', required=True) + property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') + offer_ids = fields.One2many( + 'estate.property.offer', + 'property_type_id', + string='Offers' + ) + offer_count = fields.Integer( + string=' Offers Count', + compute='_compute_offer_count' + ) + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) + + def action_open_offers(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'name': 'Property Offers', + 'res_model': 'estate.property.offer', + 'view_mode': 'tree,form', + 'domain': [('property_type_id', '=', self.id)], + } + + _sql_constraints = [ + ('unique_type_name', 'UNIQUE(name)', 'The property type name must be unique.') + ] diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 0000000000..fff544eca5 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,12 @@ +from odoo import models, fields + + +class ResUsers(models.Model): + _inherit = 'res.users' + + property_ids = fields.One2many( + 'estate.property', + 'seller_id', + string='Properties', + domain=[('state', 'not in', ['sold', 'canceled'])] + ) diff --git a/estate/report/estate_property_offer_template.xml b/estate/report/estate_property_offer_template.xml new file mode 100644 index 0000000000..a0ccaf7c2e --- /dev/null +++ b/estate/report/estate_property_offer_template.xml @@ -0,0 +1,35 @@ + + + diff --git a/estate/report/estate_property_report.xml b/estate/report/estate_property_report.xml new file mode 100644 index 0000000000..65c6b38792 --- /dev/null +++ b/estate/report/estate_property_report.xml @@ -0,0 +1,23 @@ + + + Print Property + estate.property + qweb-html + estate.report_property_offers + estate.report_property_offers + 'Estate_property_offers - %s' % (object.name or 'Attendee').replace('/','') + + report + + + + User Property Offers Report + res.users + qweb-html + estate.report_user_property_offers + estate.report_user_property_offers + 'User Estate Property - %s' % (object.name or 'Attendee').replace('/','') + + report + + diff --git a/estate/report/estate_property_template.xml b/estate/report/estate_property_template.xml new file mode 100644 index 0000000000..fdfefb79d3 --- /dev/null +++ b/estate/report/estate_property_template.xml @@ -0,0 +1,67 @@ + + + + diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 0000000000..d6b7e638e3 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,14 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1 +access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1 +access_estate_add_offer_wizard,access_estate_add_offer_wizard,model_estate_add_offer_wizard,base.group_user,1,1,1,1 +access_estate_property_agent,access_estate_property_agent,model_estate_property,estate.estate_group_user,1,1,1,0 +access_estate_property_type_agent,access_estate_property_type_agent,model_estate_property_type,estate.estate_group_user,1,0,0,0 +access_estate_property_tag_agent,access_estate_property_tag_agent,model_estate_property_tag,estate.estate_group_user,1,0,0,0 +access_estate_property_offer_agent,access_estate_property_offer_agent,model_estate_property_offer,estate.estate_group_user,1,1,1,0 +access_estate_property_type_manager,access_estate_property_type_manager,model_estate_property_type,estate.estate_group_manager,1,1,1,1 +access_estate_property_tag_manager,access_estate_property_tag_manager,model_estate_property_tag,estate.estate_group_manager,1,1,1,1 +access_estate_property_offer_manager,access_estate_property_offer_manager,model_estate_property_offer,estate.estate_group_manager,1,1,1,1 +access_estate_property_manager,access_estate_property_manager,model_estate_property,estate.estate_group_manager,1,1,1,1 diff --git a/estate/security/security.xml b/estate/security/security.xml new file mode 100644 index 0000000000..7e6c4cfa56 --- /dev/null +++ b/estate/security/security.xml @@ -0,0 +1,33 @@ + + + Agent + + + + + Manager + + + + + + Estate Property: Agent Rule + + + + ['|', ('seller_id', '=', user.id), ('seller_id', '=', False)] + + + + + Estate Property: Manager Rule + + + + + + Estate Property Multi-Company + + ['|', ('company_id', '=', False), ('company_id', 'in', company_ids)] + + diff --git a/estate/static/description/icon.png b/estate/static/description/icon.png new file mode 100644 index 0000000000..de29c6e113 Binary files /dev/null and b/estate/static/description/icon.png differ diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml new file mode 100644 index 0000000000..5ab17523f2 --- /dev/null +++ b/estate/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml new file mode 100644 index 0000000000..cb379e932c --- /dev/null +++ b/estate/views/estate_property_offers_views.xml @@ -0,0 +1,40 @@ + + + estate.property.offer.form + estate.property.offer + +
+ + + + + + + + + +
+
+
+ + estate.property.offer.list + estate.property.offer + + + + + + + + + + + + + Property Offers + estate.property.offer + tree,form + [('property_type_id', '=', active_id)] + +
diff --git a/estate/views/estate_property_tags_views.xml b/estate/views/estate_property_tags_views.xml new file mode 100644 index 0000000000..8b6137c086 --- /dev/null +++ b/estate/views/estate_property_tags_views.xml @@ -0,0 +1,31 @@ + + + estate.property.tag.form + estate.property.tag + +
+ + + + + +
+
+
+ + + estate.property.tag.list + estate.property.tag + + + + + + + + + Property Tags + estate.property.tag + tree,form + +
diff --git a/estate/views/estate_property_types_views.xml b/estate/views/estate_property_types_views.xml new file mode 100644 index 0000000000..3e412e35e4 --- /dev/null +++ b/estate/views/estate_property_types_views.xml @@ -0,0 +1,48 @@ + + + Property Types + estate.property.type + tree,form + + + estate.property.type.form + estate.property.type + +
+
+ +
+ + + + + + + + + + + + + + + + +
+
+
+ + estate.property.type.list + estate.property.type + + + + + + + + +
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 0000000000..008170b739 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,193 @@ + + + + Properties + estate.property + tree,form,kanban + {'search_default_available_properties':1} + + + estate.property.kanban + estate.property + + + + + + + + + +
+
+ + + +
+
+
+ Best Price: + +
+
+ Selling Price: + +
+
+ Expected Price: + +
+
+
+ +
+
+
+
+
+
+
+ + estate.property.list + estate.property + + + + + + + + + + + + + + +
+
+ +
+
+
+ + + estate.property.form + estate.property + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + x`x` + + + + + + + + + + + + + + + + + + + + +