From b9b3537b13fba0365f6190fd6c8f0e861e272e18 Mon Sep 17 00:00:00 2001 From: "raimundops.jr" Date: Sun, 28 Sep 2025 16:17:40 +0000 Subject: [PATCH 01/25] [ADD]fleet_traffic_infractions_account: add new module --- fleet_traffic_infractions_account/__init__.py | 1 + .../__manifest__.py | 23 ++ .../models/__init__.py | 9 + ...fleet_traffic_infraction_invoicing_term.py | 64 +++++ ..._traffic_infraction_invoicing_term_line.py | 35 +++ .../models/fleet_traffic_infraction_type.py | 16 ++ .../models/fleet_traffic_infractions.py | 226 ++++++++++++++++++ .../models/res_company.py | 22 ++ .../models/res_config_settings.py | 13 + .../readme/CONFIGURE.md | 13 + .../readme/DESCRIPTION.md | 10 + .../readme/INSTALL.md | 7 + .../security/ir.model.access.csv | 3 + ...raffic_infraction_invoicing_term_views.xml | 69 ++++++ .../fleet_traffic_infraction_type_views.xml | 17 ++ .../views/fleet_traffic_infraction_views.xml | 28 +++ .../views/res_config_settings_views.xml | 28 +++ test-requirements.txt | 5 + 18 files changed, 589 insertions(+) create mode 100644 fleet_traffic_infractions_account/__init__.py create mode 100644 fleet_traffic_infractions_account/__manifest__.py create mode 100644 fleet_traffic_infractions_account/models/__init__.py create mode 100644 fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py create mode 100644 fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term_line.py create mode 100644 fleet_traffic_infractions_account/models/fleet_traffic_infraction_type.py create mode 100644 fleet_traffic_infractions_account/models/fleet_traffic_infractions.py create mode 100644 fleet_traffic_infractions_account/models/res_company.py create mode 100644 fleet_traffic_infractions_account/models/res_config_settings.py create mode 100644 fleet_traffic_infractions_account/readme/CONFIGURE.md create mode 100644 fleet_traffic_infractions_account/readme/DESCRIPTION.md create mode 100644 fleet_traffic_infractions_account/readme/INSTALL.md create mode 100644 fleet_traffic_infractions_account/security/ir.model.access.csv create mode 100644 fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml create mode 100644 fleet_traffic_infractions_account/views/fleet_traffic_infraction_type_views.xml create mode 100644 fleet_traffic_infractions_account/views/fleet_traffic_infraction_views.xml create mode 100644 fleet_traffic_infractions_account/views/res_config_settings_views.xml create mode 100644 test-requirements.txt diff --git a/fleet_traffic_infractions_account/__init__.py b/fleet_traffic_infractions_account/__init__.py new file mode 100644 index 00000000..9a7e03ed --- /dev/null +++ b/fleet_traffic_infractions_account/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/fleet_traffic_infractions_account/__manifest__.py b/fleet_traffic_infractions_account/__manifest__.py new file mode 100644 index 00000000..45b3c175 --- /dev/null +++ b/fleet_traffic_infractions_account/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Fleet Traffic Infractions - Accounting", + "version": "18.0.1.0.0", + "category": "Fleet", + "summary": "Handle invoicing and billing for fleet traffic infractions.", + "author": "Raimundo Pereira da Silva Junior, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/fleet", + "license": "AGPL-3", + "depends": [ + "fleet_traffic_infractions", + "account", + ], + "data": [ + "security/ir.model.access.csv", + "views/res_config_settings_views.xml", + "views/fleet_traffic_infraction_views.xml", + "views/fleet_traffic_infraction_type_views.xml", + "views/fleet_traffic_infraction_invoicing_term_views.xml", + ], + "installable": True, +} \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/__init__.py b/fleet_traffic_infractions_account/models/__init__.py new file mode 100644 index 00000000..a8336407 --- /dev/null +++ b/fleet_traffic_infractions_account/models/__init__.py @@ -0,0 +1,9 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import fleet_traffic_infraction_type +from . import fleet_traffic_infraction_invoicing_term_line +from . import fleet_traffic_infraction_invoicing_term +from . import fleet_traffic_infractions +from . import res_company +from . import res_config_settings \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py new file mode 100644 index 00000000..2978c91b --- /dev/null +++ b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py @@ -0,0 +1,64 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class FleetTrafficInfractionInvoicingTerm(models.Model): + _name = "fleet.traffic.infraction.invoicing.term" + _description = "Fleet Infraction Invoicing Terms" + _order = "sequence, id" + + name = fields.Char(required=True) + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + driver_domain = fields.Char( + string="Applies on Drivers", + default="[]", + help="Domain to select the drivers to whom this term applies. " + "Leave empty to apply to all drivers.", + ) + action = fields.Selection( + [ + ("invoice_driver", "Invoice Driver"), + ("company_pays", "Company Pays (No Driver Invoice)"), + ], + required=True, + default="invoice_driver", + ) + + expense_line_ids = fields.One2many( + "fleet.traffic.infraction.invoicing.term.line", + "invoicing_term_id", + string="Additional Expenses", + ) + + discount_type = fields.Selection( + [ + ("none", "No Discount"), + ("percentage", "Percentage"), + ("fixed", "Fixed Amount"), + ], + default="none", + ) + discount_value = fields.Float() + discount_product_id = fields.Many2one( + "product.product", + string="Discount Product", + domain="[('type', '=', 'service')]", + help="A service product used to represent the discount on the invoice.", + ) + + @api.constrains("discount_type", "discount_value", "discount_product_id") + def _check_discount(self): + for term in self: + if term.discount_type != "none": + if not term.discount_product_id: + raise ValidationError( + _("You must select a Discount Product for a discount policy.") + ) + if term.discount_value <= 0: + raise ValidationError( + _("Discount value must be greater than zero.") + ) \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term_line.py b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term_line.py new file mode 100644 index 00000000..5c15c752 --- /dev/null +++ b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term_line.py @@ -0,0 +1,35 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FleetTrafficInfractionInvoicingTermLine(models.Model): + _name = "fleet.traffic.infraction.invoicing.term.line" + _description = "Fleet Infraction Invoicing Term Line (Expenses)" + + invoicing_term_id = fields.Many2one( + "fleet.traffic.infraction.invoicing.term", + string="Invoicing Term", + required=True, + ondelete="cascade", + ) + product_id = fields.Many2one( + "product.product", + string="Product", + required=True, + domain="[('type', '=', 'service')]", + ) + calculation_method = fields.Selection( + [ + ("fixed", "Fixed Amount"), + ("percentage_fine", "Percentage of Fine Amount"), + ], + string="Calculation", + required=True, + default="fixed", + ) + amount = fields.Float( + string="Amount / Percentage", + help="The fixed amount or the percentage (e.g., 10 for 10%).", + ) \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/fleet_traffic_infraction_type.py b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_type.py new file mode 100644 index 00000000..ffba4674 --- /dev/null +++ b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_type.py @@ -0,0 +1,16 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class FleetTrafficInfractionType(models.Model): + _inherit = "fleet.traffic.infraction.type" + + product_id = fields.Many2one( + "product.product", + string="Fine Product", + domain="[('type', '=', 'service')]", + help="Product used for invoicing this type of infraction. " + "Its expense account will be used for journal entries.", + ) \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/fleet_traffic_infractions.py b/fleet_traffic_infractions_account/models/fleet_traffic_infractions.py new file mode 100644 index 00000000..85eb51c5 --- /dev/null +++ b/fleet_traffic_infractions_account/models/fleet_traffic_infractions.py @@ -0,0 +1,226 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools.safe_eval import safe_eval + + +class FleetTrafficInfractions(models.Model): + _inherit = "fleet.traffic.infractions" + + state = fields.Selection( + selection_add=[ + ("invoice", "To Invoice Driver"), + ("bill", "To Create Agency Bill"), + ("processed", "Processed"), + ], + ondelete={ + "invoice": "set default", + "bill": "set default", + "processed": "set default", + }, + ) + + driver_invoice_id = fields.Many2one( + "account.move", "Driver Invoice", readonly=True, copy=False + ) + agency_move_id = fields.Many2one( + "account.move", "Agency Accounting Entry", readonly=True, copy=False + ) + + def action_view_driver_invoice(self): + self.ensure_one() + return { + "name": _("Driver Invoice"), + "view_mode": "form", + "res_model": "account.move", + "res_id": self.driver_invoice_id.id, + "type": "ir.actions.act_window", + } + + def action_view_agency_move(self): + self.ensure_one() + return { + "name": _("Agency Accounting Entry"), + "view_mode": "form", + "res_model": "account.move", + "res_id": self.agency_move_id.id, + "type": "ir.actions.act_window", + } + + def button_confirm(self): + # Overridden to move to the first accounting state + super().button_confirm() + self.write({"state": "invoice"}) + + def _prepare_invoice_line(self, product): + self.ensure_one() + line_name = f"{self.infraction_type_id.name} - {self.infraction_key}" + return { + "product_id": product.id, + "name": line_name, + "price_unit": self.fine_amount, + "quantity": 1, + } + + def _get_fine_product(self): + self.ensure_one() + product = self.infraction_type_id.product_id + if not product: + raise UserError( + _( + "Please configure a 'Fine Product' on the Infraction Type '%s' " + "before creating an invoice or bill." + ) + % self.infraction_type_id.name + ) + return product + + def _get_invoicing_term(self): + self.ensure_one() + if not self.driver_id: + return self.env["fleet.traffic.infraction.invoicing.term"] + terms = self.env["fleet.traffic.infraction.invoicing.term"].search([]) + for term in terms: + domain = safe_eval(term.driver_domain or "[]") + if self.driver_id.filtered_domain(domain): + return term + return self.env["fleet.traffic.infraction.invoicing.term"] + + def _prepare_invoice_lines_from_term(self, term): + self.ensure_one() + invoice_lines = [] + product = self._get_fine_product() + invoice_lines.append((0, 0, self._prepare_invoice_line(product))) + for line in term.expense_line_ids: + price = line.amount + if line.calculation_method == "percentage_fine": + price = (self.fine_amount * line.amount) / 100.0 + invoice_lines.append( + (0, 0, {"product_id": line.product_id.id, "price_unit": price}) + ) + if term.discount_type != "none": + discount_amount = term.discount_value + if term.discount_type == "percentage": + discount_amount = (self.fine_amount * term.discount_value) / 100.0 + invoice_lines.append( + ( + 0, + 0, + { + "product_id": term.discount_product_id.id, + "name": _("Discount"), + "price_unit": -discount_amount, + }, + ) + ) + return invoice_lines + + def create_driver_invoice(self): + for infraction in self: + if not infraction.driver_id: + raise UserError(_("A driver must be assigned to create an invoice.")) + term = infraction._get_invoicing_term() + if not term: + raise UserError( + _("No applicable Invoicing Term found for driver %s.") + % infraction.driver_id.name + ) + if term.action == "company_pays": + infraction.message_post( + body=_( + "Driver invoice skipped as per Invoicing Term '%s'. " + "The company will pay the fine." + ) + % term.name + ) + infraction.write({"state": "bill"}) + continue + if term.action == "invoice_driver": + invoice_line_vals = infraction._prepare_invoice_lines_from_term(term) + invoice = self.env["account.move"].create( + { + "partner_id": infraction.driver_id.id, + "move_type": "out_invoice", + "invoice_date": fields.Date.today(), + "invoice_origin": infraction.name, + "invoice_line_ids": invoice_line_vals, + } + ) + infraction.write({"driver_invoice_id": invoice.id, "state": "bill"}) + return True + + def create_agency_entry(self): + for infraction in self: + if not infraction.issuing_agency_id: + raise UserError(_("An issuing agency must be set to create a bill.")) + if infraction.company_id.infraction_bill_method == "vendor_bill": + infraction._create_agency_vendor_bill() + else: + infraction._create_agency_journal_entry() + return True + + def _create_agency_vendor_bill(self): + self.ensure_one() + product = self._get_fine_product() + bill = self.env["account.move"].create( + { + "partner_id": self.issuing_agency_id.id, + "move_type": "in_invoice", + "invoice_date": fields.Date.today(), + "invoice_date_due": self.due_date, + "invoice_origin": self.name, + "invoice_line_ids": [(0, 0, self._prepare_invoice_line(product))], + } + ) + self.write({"agency_move_id": bill.id, "state": "processed"}) + + def _create_agency_journal_entry(self): + self.ensure_one() + product = self._get_fine_product() + debit_account = product.product_tmpl_id.get_product_accounts()["expense"] + if not debit_account: + raise UserError( + _("No expense account is configured for product '%s'.") % product.name + ) + credit_account = self.issuing_agency_id.property_account_payable_id + journal = self.env["account.journal"].search( + [("type", "=", "purchase"), ("company_id", "=", self.company_id.id)], + limit=1, + ) + if not journal: + raise UserError(_("No purchase journal found for this company.")) + move_lines = [ + ( + 0, + 0, + { + "name": self.name, + "account_id": debit_account.id, + "debit": self.fine_amount, + "credit": 0.0, + }, + ), + ( + 0, + 0, + { + "name": self.name, + "partner_id": self.issuing_agency_id.id, + "account_id": credit_account.id, + "debit": 0.0, + "credit": self.fine_amount, + }, + ), + ] + entry = self.env["account.move"].create( + { + "journal_id": journal.id, + "move_type": "entry", + "date": fields.Date.today(), + "ref": self.name, + "line_ids": move_lines, + } + ) + self.write({"agency_move_id": entry.id, "state": "processed"}) \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/res_company.py b/fleet_traffic_infractions_account/models/res_company.py new file mode 100644 index 00000000..5d74d8bb --- /dev/null +++ b/fleet_traffic_infractions_account/models/res_company.py @@ -0,0 +1,22 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + infraction_bill_method = fields.Selection( + [ + ("vendor_bill", "Create Vendor Bill"), + ("journal_entry", "Create Miscellaneous Journal Entry"), + ], + string="Infraction Agency Billing Method", + default="vendor_bill", + help="Choose the default method to record fines from issuing agencies.\n" + "- Vendor Bill: Treats the fine as a standard purchase, affecting " + "procurement reports.\n" + "- Journal Entry: Treats the fine as a direct expense, bypassing " + "the procurement process.", + ) \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/res_config_settings.py b/fleet_traffic_infractions_account/models/res_config_settings.py new file mode 100644 index 00000000..7c9851f6 --- /dev/null +++ b/fleet_traffic_infractions_account/models/res_config_settings.py @@ -0,0 +1,13 @@ +# Copyright 2025 Raimundo Pereira da Silva Junior, Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + infraction_bill_method = fields.Selection( + related="company_id.infraction_bill_method", + readonly=False, + ) \ No newline at end of file diff --git a/fleet_traffic_infractions_account/readme/CONFIGURE.md b/fleet_traffic_infractions_account/readme/CONFIGURE.md new file mode 100644 index 00000000..1895d7e2 --- /dev/null +++ b/fleet_traffic_infractions_account/readme/CONFIGURE.md @@ -0,0 +1,13 @@ +This module extends the configuration of `fleet_traffic_infractions`. + +1. **Configure Infraction Types for Accounting:** + * Navigate to `Fleet > Configuration > Infraction Types`. + * For each type, select a **Fine Product**. This service product is essential for creating invoices and bills. + * Ensure the selected product has an **Expense Account** defined on its product form or category, as this will be used when creating journal entries. + +2. **Set the Company-Wide Billing Method:** + * Navigate to `Fleet > Configuration > Settings`. + * Under the **Traffic Infractions** section, you will find the **Infraction Agency Billing Method**. + * Choose one of two options: + * **Create Vendor Bill:** This is the default. It will create a standard supplier bill, which is useful for tracking procurement. + * **Create Miscellaneous Journal Entry:** This option creates a direct journal entry, debiting the expense account from the fine product and crediting the agency's payable account. This is useful for treating fines as taxes and keeping them out of procurement reports. \ No newline at end of file diff --git a/fleet_traffic_infractions_account/readme/DESCRIPTION.md b/fleet_traffic_infractions_account/readme/DESCRIPTION.md new file mode 100644 index 00000000..4f7a6e9b --- /dev/null +++ b/fleet_traffic_infractions_account/readme/DESCRIPTION.md @@ -0,0 +1,10 @@ +This module bridges `fleet_traffic_infractions` with Odoo's accounting features. + +**Key Features:** + +* Extends the infraction workflow with states for invoicing and billing. +* Enables the creation of a customer invoice to charge the responsible driver for the fine, plus any additional fees defined in Invoicing Terms. +* Allows configurable handling of payments to the issuing agency: + * Create a standard **Vendor Bill**. + * Create a **Miscellaneous Journal Entry** to treat the fine as a direct expense or tax. +* Adds all necessary fields and views to manage the financial aspects of traffic infractions. \ No newline at end of file diff --git a/fleet_traffic_infractions_account/readme/INSTALL.md b/fleet_traffic_infractions_account/readme/INSTALL.md new file mode 100644 index 00000000..5dda7dc1 --- /dev/null +++ b/fleet_traffic_infractions_account/readme/INSTALL.md @@ -0,0 +1,7 @@ +This module requires the `fleet_traffic_infractions` core module to be installed. + +To install this module, you need to: + +1. Add this module to your Odoo addons path. +2. Restart the Odoo server. +3. Go to the "Apps" menu, find the module, and click "Install". \ No newline at end of file diff --git a/fleet_traffic_infractions_account/security/ir.model.access.csv b/fleet_traffic_infractions_account/security/ir.model.access.csv new file mode 100644 index 00000000..0b3d7bb2 --- /dev/null +++ b/fleet_traffic_infractions_account/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 +access_fleet_traffic_infraction_invoicing_term_manager,fleet.traffic.infraction.invoicing.term.manager,model_fleet_traffic_infraction_invoicing_term,fleet.fleet_group_manager,1,1,1,1 +access_fleet_traffic_infraction_invoicing_term_line_manager,fleet.traffic.infraction.invoicing.term.line.manager,model_fleet_traffic_infraction_invoicing_term_line,fleet.fleet_group_manager,1,1,1,1 \ No newline at end of file diff --git a/fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml new file mode 100644 index 00000000..17068364 --- /dev/null +++ b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml @@ -0,0 +1,69 @@ + + + + + fleet.traffic.infraction.invoicing.term.form + fleet.traffic.infraction.invoicing.term + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + fleet.traffic.infraction.invoicing.term.tree + fleet.traffic.infraction.invoicing.term + + + + + + + + + + + + Invoicing Terms + fleet.traffic.infraction.invoicing.term + tree,form + + + + +
\ No newline at end of file diff --git a/fleet_traffic_infractions_account/views/fleet_traffic_infraction_type_views.xml b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_type_views.xml new file mode 100644 index 00000000..aff94bc8 --- /dev/null +++ b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_type_views.xml @@ -0,0 +1,17 @@ + + + + + fleet.traffic.infraction.type.form.inherit.account + fleet.traffic.infraction.type + + + + + + + + + + + \ No newline at end of file diff --git a/fleet_traffic_infractions_account/views/fleet_traffic_infraction_views.xml b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_views.xml new file mode 100644 index 00000000..e74b75d4 --- /dev/null +++ b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_views.xml @@ -0,0 +1,28 @@ + + + + + fleet.traffic.infractions.form.inherit.account + fleet.traffic.infractions + + + +
+
+
+ + + + + + +
+
+ +
\ No newline at end of file diff --git a/fleet_traffic_infractions_account/views/res_config_settings_views.xml b/fleet_traffic_infractions_account/views/res_config_settings_views.xml new file mode 100644 index 00000000..6605562f --- /dev/null +++ b/fleet_traffic_infractions_account/views/res_config_settings_views.xml @@ -0,0 +1,28 @@ + + + + res.config.settings.view.form.inherit.fleet.infraction + res.config.settings + + + +

Traffic Infractions

+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..71af21d1 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,5 @@ +# This file is used to install test dependencies. +# It is executed by the oca-install-test-addons script. + +odoo18-addon-fleet_traffic_infractions @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_traffic_infractions +odoo18-addon-fleet_vehicle_assignation_log_datetime @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_vehicle_assignation_log_datetime \ No newline at end of file From a43b43babf8f156baad2811e42f217bbe2053953 Mon Sep 17 00:00:00 2001 From: "raimundops.jr" Date: Sun, 28 Sep 2025 16:47:19 +0000 Subject: [PATCH 02/25] ext --- ...fleet_traffic_infraction_invoicing_term.py | 21 ++- .../models/fleet_traffic_infractions.py | 123 ++++++------------ .../readme/CONFIGURE.md | 21 ++- ...raffic_infraction_invoicing_term_views.xml | 89 ++++++++++--- 4 files changed, 137 insertions(+), 117 deletions(-) diff --git a/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py index 2978c91b..b698ee8e 100644 --- a/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py +++ b/fleet_traffic_infractions_account/models/fleet_traffic_infraction_invoicing_term.py @@ -7,17 +7,21 @@ class FleetTrafficInfractionInvoicingTerm(models.Model): _name = "fleet.traffic.infraction.invoicing.term" - _description = "Fleet Infraction Invoicing Terms" + _description = "Fleet Infraction Invoicing Term (Rule)" _order = "sequence, id" - name = fields.Char(required=True) - sequence = fields.Integer(default=10) + name = fields.Char(required=True, help="A descriptive name for this invoicing rule.") + sequence = fields.Integer( + default=10, + help="The priority of the rule. Lower numbers are checked first. " + "The first rule that matches the driver will be applied.", + ) active = fields.Boolean(default=True) driver_domain = fields.Char( - string="Applies on Drivers", + string="Applies To Drivers", default="[]", - help="Domain to select the drivers to whom this term applies. " - "Leave empty to apply to all drivers.", + help="Use the domain builder to define which drivers this rule applies to. " + "Leave empty to create a 'catch-all' rule.", ) action = fields.Selection( [ @@ -26,12 +30,14 @@ class FleetTrafficInfractionInvoicingTerm(models.Model): ], required=True, default="invoice_driver", + help="The action to take when this rule is matched.", ) expense_line_ids = fields.One2many( "fleet.traffic.infraction.invoicing.term.line", "invoicing_term_id", string="Additional Expenses", + help="Add lines for administrative fees or other charges to be added to the driver's invoice.", ) discount_type = fields.Selection( @@ -40,9 +46,10 @@ class FleetTrafficInfractionInvoicingTerm(models.Model): ("percentage", "Percentage"), ("fixed", "Fixed Amount"), ], + string="Discount Type", default="none", ) - discount_value = fields.Float() + discount_value = fields.Float("Discount Value") discount_product_id = fields.Many2one( "product.product", string="Discount Product", diff --git a/fleet_traffic_infractions_account/models/fleet_traffic_infractions.py b/fleet_traffic_infractions_account/models/fleet_traffic_infractions.py index 85eb51c5..be906f4e 100644 --- a/fleet_traffic_infractions_account/models/fleet_traffic_infractions.py +++ b/fleet_traffic_infractions_account/models/fleet_traffic_infractions.py @@ -28,115 +28,70 @@ class FleetTrafficInfractions(models.Model): agency_move_id = fields.Many2one( "account.move", "Agency Accounting Entry", readonly=True, copy=False ) + # This field is for display/informational purposes on the form + applied_invoicing_term_id = fields.Many2one( + "fleet.traffic.infraction.invoicing.term", + string="Applied Invoicing Rule", + compute="_compute_applied_invoicing_term_id", + store=True, + ) - def action_view_driver_invoice(self): - self.ensure_one() - return { - "name": _("Driver Invoice"), - "view_mode": "form", - "res_model": "account.move", - "res_id": self.driver_invoice_id.id, - "type": "ir.actions.act_window", - } - - def action_view_agency_move(self): - self.ensure_one() - return { - "name": _("Agency Accounting Entry"), - "view_mode": "form", - "res_model": "account.move", - "res_id": self.agency_move_id.id, - "type": "ir.actions.act_window", - } - - def button_confirm(self): - # Overridden to move to the first accounting state - super().button_confirm() - self.write({"state": "invoice"}) - - def _prepare_invoice_line(self, product): - self.ensure_one() - line_name = f"{self.infraction_type_id.name} - {self.infraction_key}" - return { - "product_id": product.id, - "name": line_name, - "price_unit": self.fine_amount, - "quantity": 1, - } - - def _get_fine_product(self): - self.ensure_one() - product = self.infraction_type_id.product_id - if not product: - raise UserError( - _( - "Please configure a 'Fine Product' on the Infraction Type '%s' " - "before creating an invoice or bill." - ) - % self.infraction_type_id.name - ) - return product + @api.depends("driver_id") + def _compute_applied_invoicing_term_id(self): + """Find and set the first matching invoicing term for the driver.""" + for infraction in self: + infraction.applied_invoicing_term_id = infraction._get_invoicing_term() def _get_invoicing_term(self): + """ + Find the first invoicing term that matches the driver of the infraction. + Terms are evaluated in ascending order of their sequence. + """ self.ensure_one() if not self.driver_id: return self.env["fleet.traffic.infraction.invoicing.term"] - terms = self.env["fleet.traffic.infraction.invoicing.term"].search([]) + + # Search for all active terms, ordered by sequence. + # The lowest sequence number has the highest priority. + terms = self.env["fleet.traffic.infraction.invoicing.term"].search( + [("active", "=", True)], order="sequence asc" + ) + for term in terms: - domain = safe_eval(term.driver_domain or "[]") - if self.driver_id.filtered_domain(domain): - return term - return self.env["fleet.traffic.infraction.invoicing.term"] + try: + domain = safe_eval(term.driver_domain or "[]") + # Check if the current driver matches the domain of the term + if self.driver_id.filtered_domain(domain): + return term # Return the first matching term + except Exception: + # Ignore terms with invalid domains + continue - def _prepare_invoice_lines_from_term(self, term): - self.ensure_one() - invoice_lines = [] - product = self._get_fine_product() - invoice_lines.append((0, 0, self._prepare_invoice_line(product))) - for line in term.expense_line_ids: - price = line.amount - if line.calculation_method == "percentage_fine": - price = (self.fine_amount * line.amount) / 100.0 - invoice_lines.append( - (0, 0, {"product_id": line.product_id.id, "price_unit": price}) - ) - if term.discount_type != "none": - discount_amount = term.discount_value - if term.discount_type == "percentage": - discount_amount = (self.fine_amount * term.discount_value) / 100.0 - invoice_lines.append( - ( - 0, - 0, - { - "product_id": term.discount_product_id.id, - "name": _("Discount"), - "price_unit": -discount_amount, - }, - ) - ) - return invoice_lines + return self.env["fleet.traffic.infraction.invoicing.term"] def create_driver_invoice(self): for infraction in self: if not infraction.driver_id: raise UserError(_("A driver must be assigned to create an invoice.")) - term = infraction._get_invoicing_term() + + term = infraction.applied_invoicing_term_id if not term: raise UserError( - _("No applicable Invoicing Term found for driver %s.") + _("No applicable Invoicing Rule found for driver %s.") % infraction.driver_id.name ) + if term.action == "company_pays": infraction.message_post( body=_( - "Driver invoice skipped as per Invoicing Term '%s'. " + "Driver invoice skipped as per Invoicing Rule '%s'. " "The company will pay the fine." ) % term.name ) infraction.write({"state": "bill"}) continue + if term.action == "invoice_driver": invoice_line_vals = infraction._prepare_invoice_lines_from_term(term) invoice = self.env["account.move"].create( @@ -150,7 +105,7 @@ def create_driver_invoice(self): ) infraction.write({"driver_invoice_id": invoice.id, "state": "bill"}) return True - + def create_agency_entry(self): for infraction in self: if not infraction.issuing_agency_id: diff --git a/fleet_traffic_infractions_account/readme/CONFIGURE.md b/fleet_traffic_infractions_account/readme/CONFIGURE.md index 1895d7e2..cc445c7f 100644 --- a/fleet_traffic_infractions_account/readme/CONFIGURE.md +++ b/fleet_traffic_infractions_account/readme/CONFIGURE.md @@ -2,12 +2,19 @@ This module extends the configuration of `fleet_traffic_infractions`. 1. **Configure Infraction Types for Accounting:** * Navigate to `Fleet > Configuration > Infraction Types`. - * For each type, select a **Fine Product**. This service product is essential for creating invoices and bills. - * Ensure the selected product has an **Expense Account** defined on its product form or category, as this will be used when creating journal entries. + * For each type, select a **Fine Product**. This service product is essential for creating invoices and bills and determining the correct expense account. -2. **Set the Company-Wide Billing Method:** +2. **Set the Company-Wide Agency Billing Method:** * Navigate to `Fleet > Configuration > Settings`. - * Under the **Traffic Infractions** section, you will find the **Infraction Agency Billing Method**. - * Choose one of two options: - * **Create Vendor Bill:** This is the default. It will create a standard supplier bill, which is useful for tracking procurement. - * **Create Miscellaneous Journal Entry:** This option creates a direct journal entry, debiting the expense account from the fine product and crediting the agency's payable account. This is useful for treating fines as taxes and keeping them out of procurement reports. \ No newline at end of file + * Under the **Traffic Infractions** section, choose the default **Infraction Agency Billing Method** (Vendor Bill or Journal Entry). + +3. **Define Driver Invoicing Rules:** + * Navigate to `Fleet > Configuration > Invoicing Rules`. + * Click "New" to create a rule. Each rule defines a financial policy for a specific group of drivers. + * **Name:** Give the rule a clear name (e.g., "Contractor Surcharge," "Employee Default Policy"). + * **Sequence:** Set the priority. Rules with lower numbers are checked first. + * **Applies To Drivers:** Use the domain builder to define which drivers this rule applies to. + * *Example: To apply a rule to all drivers in a specific department, you could use a domain like `[('department_id', '=', 'Logistics')]`.* + * *Leave the domain empty (`[]`) to create a "catch-all" rule for any driver not matched by a higher-priority rule. This should have the highest sequence number.* + * **Action:** Decide whether to invoice the driver or have the company pay. + * **Invoice Lines:** If invoicing the driver, you can add extra lines for administrative fees or apply discounts. \ No newline at end of file diff --git a/fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml index 17068364..0b9147db 100644 --- a/fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml +++ b/fleet_traffic_infractions_account/views/fleet_traffic_infraction_invoicing_term_views.xml @@ -7,29 +7,39 @@
+
+
- - - - + + + + + + + - + +

+ Define which drivers this rule applies to. Rules are checked by sequence order; the first one that matches is used. +

- - - - - - - - - - - + + + + + + + + + + + @@ -49,21 +59,62 @@ - + + - Invoicing Terms + Invoicing Rules fleet.traffic.infraction.invoicing.term tree,form + +

+ Create a new Invoicing Rule. +

+ Invoicing Rules allow you to define different financial policies for different groups of drivers. + Use the domain builder to specify which drivers a rule applies to, and set the sequence to prioritize them. +

+
+``` + +**fleet_traffic_infractions_account/views/fleet_traffic_infraction_views.xml** +```xml + + + + + fleet.traffic.infractions.form.inherit.account + fleet.traffic.infractions + + + +
+
+
+ + + + + + + +
+
+
\ No newline at end of file From 1ee58e972d71da30142430342e5a7fd5b4c6ca90 Mon Sep 17 00:00:00 2001 From: "raimundops.jr" Date: Sun, 28 Sep 2025 16:48:58 +0000 Subject: [PATCH 03/25] fix test-requirements.txt --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index 71af21d1..a32e9443 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ # This file is used to install test dependencies. # It is executed by the oca-install-test-addons script. -odoo18-addon-fleet_traffic_infractions @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_traffic_infractions -odoo18-addon-fleet_vehicle_assignation_log_datetime @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_vehicle_assignation_log_datetime \ No newline at end of file +odoo-addon-fleet_traffic_infractions @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_traffic_infractions +odoo-addon-fleet_vehicle_assignation_log_datetime @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_vehicle_assignation_log_datetime \ No newline at end of file From 0025ed5e1b9a3ae82ca6f7e30c2a98b9b9c375c5 Mon Sep 17 00:00:00 2001 From: "raimundops.jr" Date: Sun, 28 Sep 2025 16:51:50 +0000 Subject: [PATCH 04/25] fix test-requirements.txt --- test-requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-requirements.txt b/test-requirements.txt index a32e9443..8d070117 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,5 @@ # This file is used to install test dependencies. # It is executed by the oca-install-test-addons script. -odoo-addon-fleet_traffic_infractions @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_traffic_infractions -odoo-addon-fleet_vehicle_assignation_log_datetime @ git+https://github.com/OCA/fleet@18.0#subdirectory=fleet_vehicle_assignation_log_datetime \ No newline at end of file +odoo-addon-fleet_traffic_infractions@ git+https://github.com/OCA/fleet@refs/pull/185/head#subdirectory=fleet_traffic_infractions +odoo-addon-fleet_vehicle_assignation_log_datetime@ git+https://github.com/OCA/fleet@refs/pull/184/head#subdirectory=fleet_vehicle_assignation_log_datetime From 97ad6e408cd949c47656e0f4408a28a6b99f963b Mon Sep 17 00:00:00 2001 From: "raimundops.jr" Date: Sun, 28 Sep 2025 17:12:24 +0000 Subject: [PATCH 05/25] fix res.partner:NotNullViolation autopost_bills --- .../models/__init__.py | 3 ++- .../models/res_partner.py | 22 +++++++++++++++++ .../views/res_config_settings_views.xml | 24 +++++++++---------- 3 files changed, 35 insertions(+), 14 deletions(-) create mode 100644 fleet_traffic_infractions_account/models/res_partner.py diff --git a/fleet_traffic_infractions_account/models/__init__.py b/fleet_traffic_infractions_account/models/__init__.py index a8336407..5d6ba062 100644 --- a/fleet_traffic_infractions_account/models/__init__.py +++ b/fleet_traffic_infractions_account/models/__init__.py @@ -6,4 +6,5 @@ from . import fleet_traffic_infraction_invoicing_term from . import fleet_traffic_infractions from . import res_company -from . import res_config_settings \ No newline at end of file +from . import res_config_settings +from . import res_partner \ No newline at end of file diff --git a/fleet_traffic_infractions_account/models/res_partner.py b/fleet_traffic_infractions_account/models/res_partner.py new file mode 100644 index 00000000..58bbdf7e --- /dev/null +++ b/fleet_traffic_infractions_account/models/res_partner.py @@ -0,0 +1,22 @@ +# Copyright 2025 Odoo Community Association (OCA) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class ResPartner(models.Model): + _inherit = "res.partner" + + # The 'account' module adds the 'autopost_bills' field as required=True. + # While it has a default in its definition, this default sometimes fails to + # apply during the setup of test suites for dependency modules, causing a + # NotNullViolation. + # By inheriting the create method, we explicitly inject the default value + # if it's missing, ensuring system-wide stability during tests without + # altering the field's original definition. + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if "autopost_bills" not in vals: + vals["autopost_bills"] = "ask" + return super().create(vals_list) \ No newline at end of file diff --git a/fleet_traffic_infractions_account/views/res_config_settings_views.xml b/fleet_traffic_infractions_account/views/res_config_settings_views.xml index 6605562f..a0a1f250 100644 --- a/fleet_traffic_infractions_account/views/res_config_settings_views.xml +++ b/fleet_traffic_infractions_account/views/res_config_settings_views.xml @@ -5,19 +5,17 @@ res.config.settings - -

Traffic Infractions

-
-
-
-