diff --git a/dental/__init__.py b/dental/__init__.py new file mode 100644 index 000000000..5607426d8 --- /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 000000000..ccf86907d --- /dev/null +++ b/dental/__manifest__.py @@ -0,0 +1,33 @@ +{ + "name": "Dental", + "version": "1.0", + "summary": "Manage dental history and appointments", + "description": "Module to manage medical history and appointments", + "author": "Akya", + "sequence": "15", + "depends": ["base", "mail", "account", "website"], + "data": [ + "security/ir.model.access.csv", + "views/dental_patient_view.xml", + "views/dental_medication_view.xml", + "views/dental_chronic_diseases_view.xml", + "views/dental_allergies_view.xml", + "views/dental_habits_view.xml", + "views/dental_medical_aid_view.xml", + "views/dental_history_view.xml", + "views/dental_menus.xml", + "views/dental_controller_view.xml", + "report/dental_patient_report.xml", + "report/dental_patient_report_template.xml", + ], + "images": [ + "static/description/icon.png", + "static/description/bag.svg", + "static/description/folder.svg", + "static/description/task.svg", + "static/description/Bill.svg", + ], + "installable": True, + "application": True, + "license": "AGPL-3", +} diff --git a/dental/controller/__init__.py b/dental/controller/__init__.py new file mode 100644 index 000000000..95db74ea1 --- /dev/null +++ b/dental/controller/__init__.py @@ -0,0 +1 @@ +from . import dental_controller diff --git a/dental/controller/dental_controller.py b/dental/controller/dental_controller.py new file mode 100644 index 000000000..dc7ee1280 --- /dev/null +++ b/dental/controller/dental_controller.py @@ -0,0 +1,105 @@ +from odoo import http +from odoo.http import request + + +class DentalController(http.Controller): + + @http.route( + ["/home/dental", "/home/dental/page/"], auth="public", website=True + ) + def display_patients(self, page=1): + patients_per_page = 4 + current_user = request.env.user + + total_patients = ( + request.env["dental.patient"] + .sudo() + .search_count([("guarantor_id", "=", current_user.partner_id.id)]) + ) + + pager = request.website.pager( + url="/home/dental", + total=total_patients, + page=page, + step=patients_per_page, + ) + + patients = ( + request.env["dental.patient"] + .sudo() + .search( + [("guarantor_id", "=", current_user.partner_id.id)], + offset=pager["offset"], + limit=patients_per_page, + ) + ) + + return request.render( + "dental.dental_patient_page", {"patients": patients, "pager": pager} + ) + + @http.route("/home/dental/", auth="public", website=True) + def display_patient_details(self, record_id): + patient = request.env["dental.patient"].sudo().browse(record_id) + + if not patient: + return request.not_found() + + return request.render( + "dental.dental_patient_details_page", {"patient": patient} + ) + + @http.route( + "/home/dental//personal", + type="http", + auth="public", + website=True, + ) + def render_dental_patient_form(self, record_id): + patient = request.env["dental.patient"].sudo().browse(record_id) + return request.render( + "dental.dental_patient_personal_details", + { + "patient": patient, + }, + ) + + @http.route("/home/dental//medical_aid", auth="user", website=True) + def medical_aid_details(self, record_id): + patient = http.request.env["dental.patient"].sudo().browse(record_id) + return http.request.render( + "dental.dental_patient_medical_aid_details", + { + "patient": patient, + }, + ) + + @http.route( + "/home/dental//medical_history", + type="http", + auth="public", + website=True, + ) + def medical_history_view(self, record_id): + patient = request.env["dental.patient"].sudo().browse(record_id) + return request.render( + "dental.dental_history_list_view", + { + "patients": patient.history_ids, + }, + ) + + @http.route( + "/home/dental//appointment", + type="http", + auth="public", + website=True, + ) + def dental_history_list_view(self, record_id): + patient = request.env["dental.patient"].sudo().browse(record_id) + return request.render( + "dental.patient_details_controller_appointment", + { + "patients": patient.history_ids, + }, + ) diff --git a/dental/models/__init__.py b/dental/models/__init__.py new file mode 100644 index 000000000..2e35cdc98 --- /dev/null +++ b/dental/models/__init__.py @@ -0,0 +1,7 @@ +from . import dental_patient +from . import dental_medication +from . import dental_chronic_diseases +from . import dental_allergies +from . import dental_habits +from . import dental_medical_aid +from . import dental_history diff --git a/dental/models/dental_allergies.py b/dental/models/dental_allergies.py new file mode 100644 index 000000000..d46326975 --- /dev/null +++ b/dental/models/dental_allergies.py @@ -0,0 +1,10 @@ +from odoo import models, fields + + +class DentalAllergies(models.Model): + _name = "dental.allergies" + _description = "Dental Allergies list " + _order = "name" + + name = fields.Char(string="Allergies", required=True) + sequence = fields.Integer(string="Sequence", default=10) diff --git a/dental/models/dental_chronic_diseases.py b/dental/models/dental_chronic_diseases.py new file mode 100644 index 000000000..9ad302fcd --- /dev/null +++ b/dental/models/dental_chronic_diseases.py @@ -0,0 +1,10 @@ +from odoo import models, fields + + +class DentalChronicDiseases(models.Model): + _name = "dental.chronic.diseases" + _description = "Dental chronic diseases list" + _order = "name" + + name = fields.Char(string="Chronic Condition", required=True) + sequence = fields.Integer(string="Sequence", default=10) diff --git a/dental/models/dental_habits.py b/dental/models/dental_habits.py new file mode 100644 index 000000000..d86657913 --- /dev/null +++ b/dental/models/dental_habits.py @@ -0,0 +1,10 @@ +from odoo import models, fields + + +class DentalHabits(models.Model): + _name = "dental.habits" + _description = "Dental habits and substance abuse list" + _order = "name" + + name = fields.Char(string="Habits/Substance Abuse", required=True) + sequence = fields.Integer(string="Sequence", default=10) diff --git a/dental/models/dental_history.py b/dental/models/dental_history.py new file mode 100644 index 000000000..a6d08e684 --- /dev/null +++ b/dental/models/dental_history.py @@ -0,0 +1,83 @@ +from odoo import models, fields +from datetime import date + + +class DentalHistory(models.Model): + _name = "dental.history" + _description = "Dental history of patient" + + history_id = fields.Many2one("dental.patient") + date = fields.Date(string="Date", default=date.today()) + name = fields.Char(string="Name", required=True) + description = fields.Char(string="Description") + tags = fields.Char(string="Tags") + patient = fields.Char() + attend = fields.Boolean(string="Did not attend", required=True) + responsible = fields.Char() + company = fields.Many2one("res.company", string="Company") + history = fields.Text(string="History") + xray_file1 = fields.Binary(string="X-ray file 1") + xray_file2 = fields.Binary(string="X-ray file 2") + clear_aligner_file1 = fields.Binary(string="Clear Aligner File 1") + clear_aligner_file2 = fields.Binary(string="Clear Aligner File 2") + habits = fields.Text(string="Habits") + extra_observation = fields.Text(string="Extra Oral Observation") + treatment_notes = fields.Text(string="Treatment Notes") + consulatation = fields.Selection( + copy=False, + selection=[ + ( + "full_consultation_and_scan", + "Full Consultation with bite-wings 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 Push") + flouride = fields.Boolean(string="Flouride") + filling_description = fields.Text(string="Filling Description") + aligner_attachment = fields.Boolean( + string="Alligner delivery and attachment placed" + ) + whitening = fields.Boolean(string="Whitening") + fissure_sealant_quantity = fields.Float(string="Fissure Sealant-Quantity") + remove_attachment = fields.Boolean(string="Attachment Removed") + alligner_follow_up_scan = fields.Boolean(string="Alligner Follow-up Scan") + other = fields.Text(string="Other") + 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") + + notes = fields.Text() diff --git a/dental/models/dental_medical_aid.py b/dental/models/dental_medical_aid.py new file mode 100644 index 000000000..d1a745dee --- /dev/null +++ b/dental/models/dental_medical_aid.py @@ -0,0 +1,14 @@ +from odoo import models, fields + + +class DentalMedication(models.Model): + _name = "dental.medical.aid" + _description = "Medications" + _order = "name" + + name = fields.Char(string="Medical Aid Name", required=True) + contact_name = fields.Char(string="Contact") + phone = fields.Char(string="Phone") + email = fields.Char(string="Email") + company = fields.Many2one("res.company", string="Company") + notes = fields.Text() diff --git a/dental/models/dental_medication.py b/dental/models/dental_medication.py new file mode 100644 index 000000000..1abd82f8f --- /dev/null +++ b/dental/models/dental_medication.py @@ -0,0 +1,9 @@ +from odoo import models, fields + + +class DentalMedication(models.Model): + _name = "dental.medication" + _description = "Medications" + _order = "name" + + name = fields.Char(string="Medication", required=True) diff --git a/dental/models/dental_patient.py b/dental/models/dental_patient.py new file mode 100644 index 000000000..03736ae12 --- /dev/null +++ b/dental/models/dental_patient.py @@ -0,0 +1,97 @@ +from odoo import models, fields, Command, api +from datetime import date + + +class DentalPatient(models.Model): + _name = "dental.patient" + _description = "Dental Patient Info" + _inherit = ["mail.thread", "mail.activity.mixin"] + + name = fields.Char(string="Name", required=True) + state = fields.Selection( + [ + ("new", "New"), + ("to_do_today", "To do today"), + ("done", "Done"), + ("to_invoice", "To invoice"), + ], + default="new", + tracking=True, + ) + guarantor_id = fields.Many2one("res.partner", string="Guarantor") + guarantor_phone = fields.Char( + string="Guarantor Phone", related="guarantor_id.phone", readonly=True + ) + guarantor_email = fields.Char( + string="Guarantor Email", related="guarantor_id.email", readonly=True + ) + guarantor_company = fields.Char( + string="Company", related="guarantor_id.parent_id.name" + ) + guarantor_tags = fields.Many2many(string="Tags", related="guarantor_id.category_id") + image = fields.Binary(string="Image") + medication_ids = fields.Many2many("dental.medication") + chronic_conditions_ids = fields.Many2many("dental.chronic.diseases") + allergy_ids = fields.Many2many("dental.allergies") + habit_ids = fields.Many2many("dental.habits", string="Habits/Substance Abuse") + hospitalised = fields.Boolean(string="Hospitalised this year?") + female = fields.Boolean(string="FEMALE") + pregnant = fields.Boolean(string="Are you pregnant?") + nursing = fields.Boolean(string="Are you nursing?") + treatment = fields.Selection( + [ + ("hormone_replacement_treatment", "Hormone Replacement Treatment"), + ("birth_control", "Birth Control"), + ("neither", "Neither"), + ] + ) + notes = fields.Char() + special_care = fields.Char(string="Under Special Care") + psychiatric_history = fields.Char(string="Psychiatric History") + medical_aid_id = fields.Many2one("dental.medical.aid", string="Medical Aid") + medical_aid_plan = fields.Char(string="Medical Aid Plan") + medical_aid_number = fields.Char(string="Medical Aid Number") + member_code = fields.Char(string="Main Member Code") + dependant_code = fields.Char(string="Dependant Code") + emergency_contact_id = fields.Many2one("res.partner", string="Emergency Contact") + emergency_contact_phone = fields.Char( + string="Mobile", related="emergency_contact_id.phone" + ) + history_ids = fields.One2many("dental.history", "history_id", string="History") + occupation = fields.Char(string="Occupation or Grade") + identity_number = fields.Char(string="Identity Number") + date_of_birth = fields.Date(string="Date of Birth") + gender = fields.Selection( + [("male", "Male"), ("female", "Female"), ("neither", "Neither")] + ) + marital_status = fields.Selection( + [ + ("single", "Single"), + ("married", "Married"), + ("divorced", "Divorced"), + ("widowed", "Widowed"), + ] + ) + tags = fields.Char(string="Tags") + company_id = fields.Many2one("res.company", string="Company") + + @api.onchange("state") + def action_sold(self): + if self.state == "to_invoice": + for record in self: + res = { + "move_type": "out_invoice", + "partner_id": record.guarantor_id.id, + "invoice_line_ids": [ + Command.create( + { + "name": record.name, + "quantity": 1, + "invoice_date": date.today(), + "price_unit": 100, + } + ), + ], + } + + self.env["account.move"].sudo().create(res) diff --git a/dental/report/dental_patient_report.xml b/dental/report/dental_patient_report.xml new file mode 100644 index 000000000..7b4d9ade1 --- /dev/null +++ b/dental/report/dental_patient_report.xml @@ -0,0 +1,14 @@ + + + + + Dental Patient + dental.patient + qweb-pdf + dental.report_patient + dental.report_patient + + report + + + \ No newline at end of file diff --git a/dental/report/dental_patient_report_template.xml b/dental/report/dental_patient_report_template.xml new file mode 100644 index 000000000..0e8ed5a15 --- /dev/null +++ b/dental/report/dental_patient_report_template.xml @@ -0,0 +1,91 @@ + + + + \ No newline at end of file diff --git a/dental/security/ir.model.access.csv b/dental/security/ir.model.access.csv new file mode 100644 index 000000000..07ed19fd3 --- /dev/null +++ b/dental/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +dental.access_dental_patient,access_dental_patient,dental.model_dental_patient,base.group_user,1,1,1,1 +dental.access_dental_medication,access_dental_medication,dental.model_dental_medication,base.group_user,1,1,1,1 +dental.access_dental_chronic_diseases,access_dental_chronic_diseases,dental.model_dental_chronic_diseases,base.group_user,1,1,1,1 +dental.access_dental_allergies,access_dental_allergies,dental.model_dental_allergies,base.group_user,1,1,1,1 +dental.access_dental_habits,access_dental_habits,dental.model_dental_habits,base.group_user,1,1,1,1 +dental.access_dental_medical_aid,access_dental_medical_aid,dental.model_dental_medical_aid,base.group_user,1,1,1,1 +dental.access_dental_history,access_dental_history,dental.model_dental_history,base.group_user,1,1,1,1 + diff --git a/dental/static/description/bag.svg b/dental/static/description/bag.svg new file mode 100644 index 000000000..148d08a22 --- /dev/null +++ b/dental/static/description/bag.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/dental/static/description/bill.svg b/dental/static/description/bill.svg new file mode 100644 index 000000000..51d1968db --- /dev/null +++ b/dental/static/description/bill.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/dental/static/description/folder.svg b/dental/static/description/folder.svg new file mode 100644 index 000000000..e25122ed5 --- /dev/null +++ b/dental/static/description/folder.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/dental/static/description/icon.svg b/dental/static/description/icon.svg new file mode 100644 index 000000000..12e8ec084 --- /dev/null +++ b/dental/static/description/icon.svg @@ -0,0 +1 @@ + diff --git a/dental/static/description/tasks.svg b/dental/static/description/tasks.svg new file mode 100644 index 000000000..d43aeaacd --- /dev/null +++ b/dental/static/description/tasks.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/dental/views/dental_allergies_view.xml b/dental/views/dental_allergies_view.xml new file mode 100644 index 000000000..e3c269a86 --- /dev/null +++ b/dental/views/dental_allergies_view.xml @@ -0,0 +1,38 @@ + + + + + Allergies + dental.allergies + tree,form + + + + dental.allergies.tree + dental.allergies + + + + + + + + + + dental.allergies.form + dental.allergies + +
+ + +

+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/dental/views/dental_chronic_diseases_view.xml b/dental/views/dental_chronic_diseases_view.xml new file mode 100644 index 000000000..84fdc860a --- /dev/null +++ b/dental/views/dental_chronic_diseases_view.xml @@ -0,0 +1,38 @@ + + + + + Chronic Diseases + dental.chronic.diseases + tree,form + + + + dental.chronic.diseases.tree + dental.chronic.diseases + + + + + + + + + + dental.chronic.diseases.form + dental.chronic.diseases + +
+ + +

+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/dental/views/dental_controller_view.xml b/dental/views/dental_controller_view.xml new file mode 100644 index 000000000..b8bf26368 --- /dev/null +++ b/dental/views/dental_controller_view.xml @@ -0,0 +1,326 @@ + + + + + + + + + + \ No newline at end of file diff --git a/dental/views/dental_habits_view.xml b/dental/views/dental_habits_view.xml new file mode 100644 index 000000000..abadd4d03 --- /dev/null +++ b/dental/views/dental_habits_view.xml @@ -0,0 +1,38 @@ + + + + + Habits/Substance Abuse + dental.habits + form,tree + + + + dental.habits.tree + dental.habits + + + + + + + + + + dental.habits.form + dental.habits + +
+ + +

+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/dental/views/dental_history_view.xml b/dental/views/dental_history_view.xml new file mode 100644 index 000000000..75bddd9c7 --- /dev/null +++ b/dental/views/dental_history_view.xml @@ -0,0 +1,164 @@ + + + + + History + dental.history + tree + + + + dental.patient.history.tree + dental.history + + + + + + + + + + + + dental.patient.history.form + dental.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
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/dental/views/dental_medical_aid_view.xml b/dental/views/dental_medical_aid_view.xml new file mode 100644 index 000000000..7e4b390ae --- /dev/null +++ b/dental/views/dental_medical_aid_view.xml @@ -0,0 +1,46 @@ + + + + + Medical Aid + dental.medical.aid + tree,form + + + + dental.medical.aid.tree + dental.medical.aid + + + + + + + + + dental.medical.aid.form + dental.medical.aid + +
+ +

+ + + + + + + + + + + +
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/dental/views/dental_medication_view.xml b/dental/views/dental_medication_view.xml new file mode 100644 index 000000000..b19369cb9 --- /dev/null +++ b/dental/views/dental_medication_view.xml @@ -0,0 +1,37 @@ + + + + + Medication + dental.medication + tree,form + + + + dental.medication.tree + dental.medication + + + + + + + + + dental.medication.form + dental.medication + +
+ + +

+
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/dental/views/dental_menus.xml b/dental/views/dental_menus.xml new file mode 100644 index 000000000..357d9e4f6 --- /dev/null +++ b/dental/views/dental_menus.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/dental/views/dental_patient_view.xml b/dental/views/dental_patient_view.xml new file mode 100644 index 000000000..a6a931d7b --- /dev/null +++ b/dental/views/dental_patient_view.xml @@ -0,0 +1,148 @@ + + + + + Dental Patients + dental.patient + kanban,tree,form + +

No patient data

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

+ + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ +
diff --git a/installment/__init__.py b/installment/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/installment/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/installment/__manifest__.py b/installment/__manifest__.py new file mode 100644 index 000000000..9638d0b85 --- /dev/null +++ b/installment/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Installment", + "version": "1.0", + "summary": "Installment", + "description": "Installment", + "author": "Akya", + "depends": ["base", "sale_subscription", "documents"], + "data": [ + "security/ir.model.access.csv", + "views/installment_plan_view.xml", + "wizard/add_emi_wizard_view.xml", + "views/res_config_settings_views.xml", + "views/sale_order_view.xml", + "views/installment_menus.xml", + 'data/product_data.xml', + ], + "installable": True, + "application": True, + "license": "AGPL-3", +} diff --git a/installment/data/product_data.xml b/installment/data/product_data.xml new file mode 100644 index 000000000..cb7487bc8 --- /dev/null +++ b/installment/data/product_data.xml @@ -0,0 +1,15 @@ + + + + Installment + True + + + + Down Payment + + + Penalty + + + \ No newline at end of file diff --git a/installment/models/__init__.py b/installment/models/__init__.py new file mode 100644 index 000000000..61be56551 --- /dev/null +++ b/installment/models/__init__.py @@ -0,0 +1,4 @@ +from . import installment_plan +from . import res_config_settings +from . import sale_order +from . import account_move diff --git a/installment/models/account_move.py b/installment/models/account_move.py new file mode 100644 index 000000000..16b435eaa --- /dev/null +++ b/installment/models/account_move.py @@ -0,0 +1,72 @@ +from datetime import timedelta +from odoo import Command, models, fields, api + + +class accountMove(models.Model): + _inherit = "account.move" + + penalty_applied = fields.Boolean(string="Applied Penalty", default=False) + + @api.model + def create_recurring_invoice(self): + + today = fields.Date.today() + delay_penalty_process = float( + self.env["ir.config_parameter"] + .get_param("installment.delay_penalty_process") + ) + + invoices = self.env["account.move"].search( + [ + ("state", "=", "posted"), + ("payment_state", "=", "not_paid"), + ("move_type", "=", "out_invoice"), + ("penalty_applied", "=", False), + ] + ) + for invoice in invoices: + + for line in invoice.line_ids: + if ( + line.product_id.id + == self.env.ref("installment.product_installment").id + ): + due_date = invoice.invoice_date_due + + deadline = due_date + timedelta(days=delay_penalty_process) + + if today >= deadline: + penalty_amount = self.calculate_penalty_amount(invoice) + + invoice_vals = { + "partner_id": invoice.partner_id.id, + "move_type": "out_invoice", + "line_ids": [ + Command.create( + { + "product_id": self.env.ref( + "installment.product_penalty" + ).id, + "name": "Penalty Amount", + "price_unit": penalty_amount, + "tax_ids": None, + } + ), + ], + } + new_invoice = ( + self.env["account.move"].create(invoice_vals) + ) + new_invoice.action_post() + invoice.write({"penalty_applied": True}) + + def calculate_penalty_amount(self, invoice): + + delay_penalty_percentage = float( + self.env["ir.config_parameter"] + .get_param("installment.delay_penalty_percentage") + ) + + penalty_amount = (invoice.amount_total * delay_penalty_percentage) / 100 + + return penalty_amount diff --git a/installment/models/installment_plan.py b/installment/models/installment_plan.py new file mode 100644 index 000000000..0848ec0ec --- /dev/null +++ b/installment/models/installment_plan.py @@ -0,0 +1,15 @@ +from odoo import models, fields + + +class InstallmentPlan(models.Model): + _name = "installment.plan" + _description = "Installments info" + + sale_order_id = fields.Many2one("sale.order", string="Sale Order") + down_payment = fields.Float(string="Down Payment", compute="_compute_down_payment") + admin_expenses = fields.Float( + string="Administrative Expenses", compute="_compute_admin_expenses" + ) + monthly_installment = fields.Float( + string="Monthly Installment", compute="_compute_monthly_installment" + ) diff --git a/installment/models/res_config_settings.py b/installment/models/res_config_settings.py new file mode 100644 index 000000000..beb8bd122 --- /dev/null +++ b/installment/models/res_config_settings.py @@ -0,0 +1,50 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + max_duration = fields.Float( + string="Max Duration (Years)", + config_parameter="installment.max_duration", + required=True, + ) + annual_rate = fields.Float( + string="Annual Rate Percentage", config_parameter="installment.annual_rate" + ) + down_payment_percentage = fields.Float( + string="Down Payment Percentage", + config_parameter="installment.down_payment_percentage", + ) + admin_expenses_percentage = fields.Float( + string="Administrative Expenses Percentage", + config_parameter="installment.admin_expenses_percentage", + ) + delay_penalty_percentage = fields.Float( + string="Delay Penalty Percentage", + config_parameter="installment.delay_penalty_percentage", + ) + delay_penalty_process = fields.Float( + string="Delay Penalty Process (Days)", + config_parameter="installment.delay_penalty_process", + ) + nid = fields.Boolean(string="NID", config_parameter="installment.nid") + bank_statement = fields.Boolean( + string="Bank Statement", config_parameter="installment.bank_statement" + ) + rental_contract = fields.Boolean( + string="Rental Contract", + config_parameter="installment.rental_contract", + ) + salary_components = fields.Boolean( + string="Salary Components", + config_parameter="installment.salary_components", + ) + bank_rate_letter = fields.Boolean( + string="Bank Rate Letter", + config_parameter="installment.bank_rate_letter", + ) + ownership_contract = fields.Boolean( + string="Ownership Contract", + config_parameter="installment.ownership_contract", + ) diff --git a/installment/models/sale_order.py b/installment/models/sale_order.py new file mode 100644 index 000000000..47e399e84 --- /dev/null +++ b/installment/models/sale_order.py @@ -0,0 +1,109 @@ +from odoo import models, fields + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + def action_upload_documents(self): + """ + This method retrieves required documents based on the configuration settings, + creates a folder for each sale order if it doesn't exist, and uploads placeholders + for the required documents in the folder. + """ + config_settings = self.env["ir.config_parameter"] + document_settings = { + "nid": "National ID (NID)", + "bank_statement": "Bank Statement", + "rental_contract": "Rental Contract", + "salary_components": "Salary Components", + "bank_rate_letter": "Bank Rate Letter", + "ownership_contract": "Ownership Contract", + } + + # Get the required documents based on the configuration settings + required_documents = [ + document_name + for setting_key, document_name in document_settings.items() + if config_settings.get_param(f"installment.{setting_key}", default=False) + ] + + for order in self: + # Get or create the Installments folder + installments_folder = self.env["documents.folder"].search( + [("name", "=", "Installments")], limit=1 + ) + if not installments_folder: + installments_folder = self.env["documents.folder"].create( + {"name": "Installments"} + ) + + # Define folder name based on sale order + folder_name = order.name.replace("/", "_") + folder = self.env["documents.folder"].search( + [ + ("name", "=", folder_name), + ("parent_folder_id", "=", installments_folder.id), + ], + limit=1, + ) + + # If folder doesn't exist, create it + if not folder: + folder = self.env["documents.folder"].create( + {"name": folder_name, "parent_folder_id": installments_folder.id} + ) + + # Create document placeholders for the required documents if they don't exist + for doc_name in required_documents: + # Search for document by folder, res_model, and res_id + existing_doc = self.env["documents.document"].search( + [ + ("folder_id", "=", folder.id), + ("res_model", "=", "sale.order"), + ("res_id", "=", order.id), + ] + ) + + if not existing_doc: + self.env["documents.document"].create( + { + "name": doc_name, + "folder_id": folder.id, + "partner_id": order.partner_id.id, + "res_model": "sale.order", + "res_id": order.id, + "is_done": False, # Document not yet uploaded + } + ) + + else: + # Check if the document is uploaded, if yes mark it as done + if existing_doc.attachment_id: + existing_doc.write({"is_done": True}) # Mark as done + + # Return an action to display the documents in the folder + return { + "type": "ir.actions.act_window", + "name": "Documents", + "res_model": "documents.document", + "view_mode": "kanban,tree,form", + "domain": [("folder_id", "=", folder.id)], + "context": {"searchpanel_default_folder_id": folder.id}, + } + + +class Document(models.Model): + _inherit = "documents.document" + + is_done = fields.Boolean(string="Document Uploaded", default=False) + + def write(self, vals): + """ + Override the write method to mark the document as 'done' when a file is uploaded. + """ + # Check if the document is getting a file uploaded + if "attachment_id" in vals: + # Set is_done to True if the document gets a file + vals["is_done"] = True + + return super().write(vals) diff --git a/installment/security/ir.model.access.csv b/installment/security/ir.model.access.csv new file mode 100644 index 000000000..4db11d88f --- /dev/null +++ b/installment/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +installment.access_installment_plan,access_installment_plan,installment.model_installment_plan,base.group_user,1,1,1,1 +installment.access_add_emi,access_add_emi,installment.model_add_emi,base.group_user,1,1,1,1 diff --git a/installment/views/installment_menus.xml b/installment/views/installment_menus.xml new file mode 100644 index 000000000..c27f94eaa --- /dev/null +++ b/installment/views/installment_menus.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/installment/views/installment_plan_view.xml b/installment/views/installment_plan_view.xml new file mode 100644 index 000000000..e25da98d7 --- /dev/null +++ b/installment/views/installment_plan_view.xml @@ -0,0 +1,11 @@ + + + + Installment Plan + installment.plan + tree + +

No patient data

+
+
+
\ No newline at end of file diff --git a/installment/views/res_config_settings_views.xml b/installment/views/res_config_settings_views.xml new file mode 100644 index 000000000..beff18c40 --- /dev/null +++ b/installment/views/res_config_settings_views.xml @@ -0,0 +1,67 @@ + + + Settings + res.config.settings + form + + + res.config.settings.view.form.inherit.installment + res.config.settings + + + + + + + + Years + + + + % Per Year + + + + % of Product Price + + + + % of Amount After Down Payment + + + + % of Monthly Amount + + + + Days +
+ Delay penalty percentage will be applied after exceeding the delay process period. +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
\ No newline at end of file diff --git a/installment/views/sale_order_view.xml b/installment/views/sale_order_view.xml new file mode 100644 index 000000000..695d20461 --- /dev/null +++ b/installment/views/sale_order_view.xml @@ -0,0 +1,33 @@ + + + + + sale.order.form.inherit.view + sale.order + + + + + + +