From b30ae9d3bb7e0489cc69f4bc32528ca72cfa0807 Mon Sep 17 00:00:00 2001 From: EdgarRetes Date: Wed, 18 Sep 2024 15:46:13 -0600 Subject: [PATCH] [ADD] tms_sale --- .eslintrc.yml | 1 + tms_sale/README.rst | 35 +++ tms_sale/__init__.py | 2 + tms_sale/__manifest__.py | 30 +++ tms_sale/models/__init__.py | 4 + tms_sale/models/sale_order.py | 204 +++++++++++++++++ tms_sale/models/sale_order_line.py | 209 ++++++++++++++++++ tms_sale/models/seat_ticket.py | 43 ++++ tms_sale/models/tms_order.py | 40 ++++ tms_sale/pyproject.toml | 3 + tms_sale/security/ir.model.access.csv | 9 + tms_sale/static/description/icon.png | Bin 0 -> 4277 bytes tms_sale/static/description/icon.svg | 80 +++++++ .../src/js/line_ticket_wizard_controller.js | 31 +++ .../src/js/line_trip_wizard_controller.js | 35 +++ .../src/js/sale_order_line_product_field.js | 150 +++++++++++++ tms_sale/views/product_template_views.xml | 36 +++ tms_sale/views/sale_order_views.xml | 40 ++++ tms_sale/views/tms_order_views.xml | 41 ++++ tms_sale/wizard/__init__.py | 2 + tms_sale/wizard/sale_order_line_trip.py | 31 +++ .../wizard/sale_order_line_trip_views.xml | 90 ++++++++ tms_sale/wizard/seat_ticket_line.py | 21 ++ tms_sale/wizard/seat_ticket_line_views.xml | 42 ++++ 24 files changed, 1179 insertions(+) create mode 100644 tms_sale/README.rst create mode 100644 tms_sale/__init__.py create mode 100644 tms_sale/__manifest__.py create mode 100644 tms_sale/models/__init__.py create mode 100644 tms_sale/models/sale_order.py create mode 100644 tms_sale/models/sale_order_line.py create mode 100644 tms_sale/models/seat_ticket.py create mode 100644 tms_sale/models/tms_order.py create mode 100644 tms_sale/pyproject.toml create mode 100644 tms_sale/security/ir.model.access.csv create mode 100644 tms_sale/static/description/icon.png create mode 100644 tms_sale/static/description/icon.svg create mode 100644 tms_sale/static/src/js/line_ticket_wizard_controller.js create mode 100644 tms_sale/static/src/js/line_trip_wizard_controller.js create mode 100644 tms_sale/static/src/js/sale_order_line_product_field.js create mode 100644 tms_sale/views/product_template_views.xml create mode 100644 tms_sale/views/sale_order_views.xml create mode 100644 tms_sale/views/tms_order_views.xml create mode 100644 tms_sale/wizard/__init__.py create mode 100644 tms_sale/wizard/sale_order_line_trip.py create mode 100644 tms_sale/wizard/sale_order_line_trip_views.xml create mode 100644 tms_sale/wizard/seat_ticket_line.py create mode 100644 tms_sale/wizard/seat_ticket_line_views.xml diff --git a/.eslintrc.yml b/.eslintrc.yml index fed88d70d..a61c67db7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -5,6 +5,7 @@ env: # See https://github.com/OCA/odoo-community.org/issues/37#issuecomment-470686449 parserOptions: ecmaVersion: 2019 + sourceType: module overrides: - files: diff --git a/tms_sale/README.rst b/tms_sale/README.rst new file mode 100644 index 000000000..38929e877 --- /dev/null +++ b/tms_sale/README.rst @@ -0,0 +1,35 @@ +**This file is going to be generated by oca-gen-addon-readme.** + +*Manual changes will be overwritten.* + +Please provide content in the ``readme`` directory: + +* **DESCRIPTION.rst** (required) +* INSTALL.rst (optional) +* CONFIGURE.rst (optional) +* **USAGE.rst** (optional, highly recommended) +* DEVELOP.rst (optional) +* ROADMAP.rst (optional) +* HISTORY.rst (optional, recommended) +* **CONTRIBUTORS.rst** (optional, highly recommended) +* CREDITS.rst (optional) + +Content of this README will also be drawn from the addon manifest, +from keys such as name, authors, maintainers, development_status, +and license. + +A good, one sentence summary in the manifest is also highly recommended. + + +Automatic changelog generation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`HISTORY.rst` can be auto generated using `towncrier `_. + +Just put towncrier compatible changelog fragments into `readme/newsfragments` +and the changelog file will be automatically generated and updated when a new fragment is added. + +Please refer to `towncrier` documentation to know more. + +NOTE: the changelog will be automatically generated when using `/ocabot merge $option`. +If you need to run it manually, refer to `OCA/maintainer-tools README `_. diff --git a/tms_sale/__init__.py b/tms_sale/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/tms_sale/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/tms_sale/__manifest__.py b/tms_sale/__manifest__.py new file mode 100644 index 000000000..da313aae5 --- /dev/null +++ b/tms_sale/__manifest__.py @@ -0,0 +1,30 @@ +# Copyright (C) 2018 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +{ + "name": "TMS - Sales", + "version": "17.0.1.0.0", + "summary": "Sell transportation management system.", + "category": "TMS", + "author": "Open Source Integrators, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/stock-logistics-transport", + "depends": ["tms", "tms_product", "sale_management", "web"], + "data": [ + "security/ir.model.access.csv", + "wizard/sale_order_line_trip_views.xml", + "wizard/seat_ticket_line_views.xml", + "views/sale_order_views.xml", + "views/tms_order_views.xml", + "views/product_template_views.xml", + ], + "assets": { + "web.assets_backend": [ + "tms_sale/static/src/js/line_trip_wizard_controller.js", + "tms_sale/static/src/js/line_ticket_wizard_controller.js", + "tms_sale/static/src/js/sale_order_line_product_field.js", + ], + }, + "license": "AGPL-3", + "development_status": "Alpha", + "maintainers": ["max3903", "santiagordz", "EdgarRetes"], + "installable": True, +} diff --git a/tms_sale/models/__init__.py b/tms_sale/models/__init__.py new file mode 100644 index 000000000..2fad64e90 --- /dev/null +++ b/tms_sale/models/__init__.py @@ -0,0 +1,4 @@ +from . import sale_order_line +from . import sale_order +from . import seat_ticket +from . import tms_order diff --git a/tms_sale/models/sale_order.py b/tms_sale/models/sale_order.py new file mode 100644 index 000000000..770fb62c9 --- /dev/null +++ b/tms_sale/models/sale_order.py @@ -0,0 +1,204 @@ +# Copyright (C) 2024 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from markupsafe import Markup + +from odoo import _, api, fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + tms_order_ids = fields.Many2many( + "tms.order", + compute="_compute_tms_order_ids", + string="Transport orders associated to this sale", + copy=False, + ) + tms_order_count = fields.Integer( + string="Transport Orders", compute="_compute_tms_order_ids" + ) + + has_tms_order = fields.Boolean(compute="_compute_has_tms_order") + + def action_view_trip_sale_order_line(self): + return { + "type": "ir.actions.act_window", + "res_model": "sale.order.line.trip", + "target": "new", + "view_mode": "form", + "view_type": "form", + } + + @api.depends("order_line") + def _compute_has_tms_order(self): + for sale in self: + has_tms_order = any( + line.product_template_id.tms_trip + and line.product_template_id.trip_product_type == "trip" + for line in sale.order_line + ) + sale.has_tms_order = has_tms_order + + @api.depends("order_line") + def _compute_tms_order_ids(self): + for sale in self: + tms = self.env["tms.order"].search( + [ + "|", + ("sale_id", "=", sale.id), + ("sale_line_id", "in", sale.order_line.ids), + ] + ) + sale.tms_order_ids = tms + sale.tms_order_count = len(sale.tms_order_ids) + + def _tms_generate_line_tms_orders(self, new_tms_sol): + """ + Generate TMS Orders for the given sale order lines. + + Override this method to filter lines to generate TMS Orders for. + """ + self.ensure_one() + new_tms_orders = self.env["tms.order"] + + for line in new_tms_sol: + for i in range(int(line.product_uom_qty) - len(line.tms_order_ids)): + vals = line._prepare_line_tms_values(line) + tms_by_line = self.env["tms.order"].sudo().create(vals) + line.write({"tms_order_ids": [(4, tms_by_line.id)]}) + new_tms_orders |= tms_by_line + i = i # pre-commit + + return new_tms_orders + + def _tms_generate(self): + self.ensure_one() + new_tms_orders = self.env["tms.order"] + + new_tms_line_sol = self.order_line.filtered( + lambda L: L.product_id.trip_product_type == "trip" + and len(L.tms_order_ids) != L.product_uom_qty + and len(L.tms_order_ids) < L.product_uom_qty + ) + + new_tms_orders |= self._tms_generate_line_tms_orders(new_tms_line_sol) + + return new_tms_orders + + def _tms_generation(self): + """ + Create TMS Orders based on the products' configuration. + :rtype: list(TMS Orders) + :return: list of newly created TMS Orders + """ + created_tms_orders = self.env["tms.order"] + + for sale in self: + new_tms_orders = self._tms_generate() + + if len(new_tms_orders) > 0: + created_tms_orders |= new_tms_orders + sale._post_tms_message(new_tms_orders) + + return created_tms_orders + + def _post_tms_message(self, tms_orders): + """ + Post messages to the Sale Order and the newly created TMS Orders + """ + self.ensure_one() + for tms_order in tms_orders: + tms_order.message_mail_with_source( + "mail.message_origin_link", + render_values={"self": tms_order, "origin": self}, + subtype_id=self.env.ref("mail.mt_note").id, + author_id=self.env.user.partner_id.id, + ) + message = _( + "Transport Order(s) Created: %s", + Markup( + f"""{tms_order.name}""" + ), + ) + self.message_post(body=message) + + def _action_create_new_trips(self): + if any( + sol.product_id.trip_product_type == "trip" + for sol in self.order_line.filtered( + lambda x: x.display_type not in ("line_section", "line_note") + ) + ): + self._tms_generation() + + def action_view_tms_order(self): + tms_orders = self.mapped("tms_order_ids") + action = self.env["ir.actions.act_window"]._for_xml_id( + "tms.action_tms_dash_order" + ) + if len(tms_orders) > 1: + action["domain"] = [("id", "in", tms_orders.ids)] + elif len(tms_orders) == 1: + action["views"] = [(self.env.ref("tms.tms_order_view_form").id, "form")] + action["res_id"] = tms_orders.id + else: + action = {"type": "ir.actions.act_window_close"} + return action + + def remove_lines_with_trips( + self, initial_trips, initial_line_ids, initial_quantities + ): + current_order_line_ids = set(self.order_line.ids) + removed_lines = initial_line_ids - current_order_line_ids + + if removed_lines: + trips_to_delete = initial_trips.filtered( + lambda trip_id: not (trip_id.sale_line_id & self.order_line) + ) + if trips_to_delete: + trips_to_delete.unlink() + + for line in self.order_line: + if line.id in initial_quantities: + if line.product_uom_qty < initial_quantities[line.id]: + trips_to_delete = line.tms_order_ids[:1] + if trips_to_delete: + trips_to_delete.unlink() + + @api.model + def create(self, vals): + order = super().create(vals) + if "order_line" in vals and order.has_tms_order: + order._action_create_new_trips() + return order + + @api.model + def write(self, vals): + initial_trips = self.env["tms.order"].search( + [("sale_line_id", "in", self.order_line.ids)] + ) + initial_order_line_ids = set(self.order_line.ids) + initial_quantities = {line.id: line.product_uom_qty for line in self.order_line} + + result = super().write(vals) + + if "order_line" in vals and self.has_tms_order: + self.remove_lines_with_trips( + initial_trips, initial_order_line_ids, initial_quantities + ) + self._action_create_new_trips() + + if "state" in vals: + if self.state == "sale": + stage = self.env.ref("tms.tms_stage_order_confirmed") + elif self.state == "cancel": + stage = self.env.ref("tms.tms_stage_order_cancelled") + else: + stage = self.env.ref("tms.tms_stage_order_draft") + + for line in self.order_line: + for trip in line.tms_order_ids: + trip.stage_id = stage + + return result diff --git a/tms_sale/models/sale_order_line.py b/tms_sale/models/sale_order_line.py new file mode 100644 index 000000000..739552640 --- /dev/null +++ b/tms_sale/models/sale_order_line.py @@ -0,0 +1,209 @@ +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + trip_line_ids = fields.One2many("sale.order.line.trip", "order_line_id") + + tms_trip_ticket_id = fields.Many2one("tms.order") + tms_ticket_ids = fields.One2many("seat.ticket", "sale_line_id") + + tms_order_ids = fields.One2many( + "tms.order", + "sale_line_id", + index=True, + copy=False, + help="Transport Order generated by the sales order item", + ) + + tms_factor_uom = fields.Char( + compute="_compute_sale_order_line_tms", store=True, readonly=False + ) + tms_factor = fields.Float( + default=1, compute="_compute_sale_order_line_tms", store=True, readonly=False + ) + + tms_route_flag = fields.Boolean(string="Use Routes", default=False) + tms_route_id = fields.Many2one("tms.route", string="Routes") + tms_origin_id = fields.Many2one( + "res.partner", + string="Origin", + domain="[('tms_location', '=', 'True')]", + context={"default_tms_location": True}, + ) + tms_destination_id = fields.Many2one( + "res.partner", + string="Destination", + domain="[('tms_location', '=', 'True')]", + context={"default_tms_location": True}, + ) + + tms_scheduled_date_start = fields.Datetime(string="Scheduled Start") + tms_scheduled_date_end = fields.Datetime(string="Scheduled End") + + has_trip_product = fields.Boolean( + readonly=True, default=False, compute="_compute_sale_order_line_tms", store=True + ) + seat_ticket = fields.Boolean( + readonly=True, default=False, compute="_compute_sale_order_line_tms", store=True + ) + + @api.depends("product_id", "product_template_id") + def _compute_sale_order_line_tms(self): + for line in self: + if line.product_template_id.tms_factor_distance_uom: + line.tms_factor_uom = ( + line.product_template_id.tms_factor_distance_uom.name + ) + if line.tms_route_id.distance: + line.tms_factor = line.tms_route_id.distance + line.tms_factor_uom = line.tms_route_id.distance_uom.name + elif line.product_template_id.tms_factor_weight_uom: + line.tms_factor_uom = ( + line.product_template_id.tms_factor_weight_uom.name + ) + else: + line.tms_factor_uom = False + + line.has_trip_product = ( + line.product_template_id.trip_product_type == "trip" + and line.product_template_id.tms_trip + and line.product_template_id.detailed_type == "service" + ) or ( + line.product_id.trip_product_type == "trip" + and line.product_id.tms_trip + and line.product_id.detailed_type == "service" + ) + + line.seat_ticket = ( + line.product_template_id.trip_product_type == "seat" + and line.product_template_id.tms_trip + and line.product_template_id.detailed_type == "service" + ) or ( + line.product_id.trip_product_type == "seat" + and line.product_id.tms_trip + and line.product_id.detailed_type == "service" + ) + + def _update_tickets(self, tickets): + return True + + @api.constrains( + "tms_route_flag", + "tms_route_id", + "tms_origin_id", + "tms_destination_id", + "tms_scheduled_date_start", + "tms_scheduled_date_end", + ) + def _check_required_fields(self): + for record in self: + if record.trip_line_ids: + if record.tms_route_flag and not record.tms_route_id: + raise ValidationError( + _("The route is not set in a trip using predefined routes.") + ) + if not record.tms_route_flag: + if not record.tms_origin_id: + raise ValidationError( + _("The origin location from a trip is not set.") + ) + if not record.tms_destination_id: + raise ValidationError( + _("The destination location from a trip is not set.") + ) + if not record.tms_scheduled_date_start: + raise ValidationError( + _("A scheduled date of start from a trip is not set.") + ) + if not record.tms_scheduled_date_end: + raise ValidationError( + _("A scheduled date of end from a trip is not set.") + ) + + if ( + record.product_template_id.detailed_type == "service" + and record.product_template_id.trip_product_type == "seat" + ): + if not record.tms_trip_ticket_id: + raise ValidationError(_("A ticket isn't assigned to a trip")) + + def _prepare_tms_values(self, **kwargs): + """ + Prepare the values to create a new TMS Order from a sale order. + """ + self.ensure_one() + duration = self.tms_scheduled_date_end - self.tms_scheduled_date_start + if isinstance(duration, int): + duration = timedelta(seconds=duration) + duration_in_s = duration.total_seconds() + hours = duration_in_s / 3600 + return { + "sale_id": kwargs.get("so_id", False), + "sale_line_id": kwargs.get("sol_id", False), + "company_id": self.company_id.id, + "route": self.tms_route_flag, + "route_id": self.tms_route_id.id or None, + "origin_id": self.tms_origin_id.id or None, + "destination_id": self.tms_destination_id.id or None, + "scheduled_date_start": self.tms_scheduled_date_start or None, + "scheduled_date_end": self.tms_scheduled_date_end or None, + "scheduled_duration": hours, + } + + def _prepare_line_tms_values(self, line): + """ + Prepare the values to create a new TMS Order from a sale order line. + """ + self.ensure_one() + vals = self._prepare_tms_values(so_id=self.order_id.id, sol_id=self.id) + return vals + + def _convert_to_tax_base_line_dict(self, **kwargs): + """Convert the current record to a dictionary in + order to use the generic taxes computation method + defined on account.tax. + + :return: A python dictionary. + """ + self.ensure_one() + return self.env["account.tax"]._convert_to_tax_base_line_dict( + self, + partner=self.order_id.partner_id, + currency=self.order_id.currency_id, + product=self.product_id, + taxes=self.tax_id, + price_unit=self.price_unit, + quantity=self.product_uom_qty * self.tms_factor, + discount=self.discount, + price_subtotal=self.price_subtotal, + **kwargs, + ) + + @api.depends("product_uom_qty", "discount", "price_unit", "tax_id", "tms_factor") + def _compute_amount(self): + return super()._compute_amount() + + @api.model + def write(self, vals): + line = super().write(vals) + for trip in self.tms_order_ids: + if "tms_route_flag" in vals: + trip.route = vals.get("tms_route_flag") + if "tms_route_id" in vals: + trip.route_id = vals.get("tms_route_id") + if "tms_origin_id" in vals: + trip.origin_id = vals.get("tms_origin_id") + if "tms_destination_id" in vals: + trip.destination_id = vals.get("tms_destination_id") + if "tms_scheduled_date_start" in vals: + trip.scheduled_date_start = vals.get("tms_scheduled_date_start") + if "tms_scheduled_date_end" in vals: + trip.scheduled_date_end = vals.get("tms_scheduled_date_end") + return line diff --git a/tms_sale/models/seat_ticket.py b/tms_sale/models/seat_ticket.py new file mode 100644 index 000000000..030a3d6aa --- /dev/null +++ b/tms_sale/models/seat_ticket.py @@ -0,0 +1,43 @@ +# Copyright (C) 2024 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class SeatTicket(models.Model): + _name = "seat.ticket" + + name = fields.Char( + copy=False, + readonly=False, + index="trigram", + default=lambda self: _("New ticket"), + ) + tms_order_id = fields.Many2one("tms.order", store=True) + sale_line_id = fields.Many2one("sale.order.line") + section = fields.Char() + price = fields.Float() + available = fields.Selection( + [("available", "Available"), ("not_available", "Not available")], + default="available", + readonly=True, + ) + + @api.model + def write(self, vals): + res = super().write(vals) + for record in self: + if "sale_line_id" in vals: + if vals.get("sale_line_id"): + record.available = "not_available" + else: + record.available = "available" + return res + + @api.model + def create(self, vals): + if vals.get("name", _("New")) == _("New ticket"): + trip = self.env["tms.order"].browse(vals["tms_order_id"]) + vals["name"] = f"{trip.name}-{len(trip.seat_ticket_ids) + 1}" + + return super().create(vals) diff --git a/tms_sale/models/tms_order.py b/tms_sale/models/tms_order.py new file mode 100644 index 000000000..42ed4bf37 --- /dev/null +++ b/tms_sale/models/tms_order.py @@ -0,0 +1,40 @@ +# Copyright (C) 2019 Brian McMaster +# Copyright (C) 2019 Open Source Integrators +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import _, api, fields, models + + +class TMSOrder(models.Model): + _inherit = "tms.order" + + sale_id = fields.Many2one("sale.order", copy=False) + sale_line_id = fields.Many2one("sale.order.line", copy=False) + seat_ticket_ids = fields.One2many("seat.ticket", "tms_order_id") + + def action_view_sales(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "res_model": "sale.order", + "views": [[False, "form"]], + "res_id": self.sale_line_id.order_id.id or self.sale_id.id, + "context": {"create": False}, + "name": _("Sales Orders"), + } + + @api.model + def write(self, vals): + for order in self: + if "stage_id" in vals: + stage = self.env.ref("tms.tms_stage_order_completed") + if vals["stage_id"] == stage.id: + for line in order.sale_id.order_line: + line.qty_delivered = line.product_uom_qty + + if "seat_ticket_ids" in vals: + tickets = vals.get("seat_ticket_ids", []) + for i in range(len(tickets)): + if tickets[i][0] == 2: + self.env["seat.ticket"].browse(tickets[i][1]).unlink() + + return super().write(vals) diff --git a/tms_sale/pyproject.toml b/tms_sale/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/tms_sale/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/tms_sale/security/ir.model.access.csv b/tms_sale/security/ir.model.access.csv new file mode 100644 index 000000000..ff064c3db --- /dev/null +++ b/tms_sale/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 +access_sale_order_tms_user,sale.order.tms.user,model_sale_order,tms.group_tms_user,1,0,0,0 +access_sale_order_tms_admin,sale.order.tms.admin,model_sale_order,tms.group_tms_admin,1,1, +access_sale_order_line_trip_tms_user,sale.order.line.trip.tms.user,model_sale_order_line_trip,tms.group_tms_user,1,0,0,0 +access_sale_order_line_trip_tms_admin,sale.order.line.trip.tms.admin,model_sale_order_line_trip,tms.group_tms_admin,1,1,1,1 +access_seat_ticket_tms_user,seat.ticket.tms.user,model_seat_ticket,tms.group_tms_user,1,0,0,0 +access_seat_ticket_tms_admin,seat.ticket.tms.admin,model_seat_ticket,tms.group_tms_admin,1,1,1,1 +access_sale_order_line_seat_ticket_tms_user,sale.order.line.seat.ticket.tms.user,model_seat_ticket_line,tms.group_tms_user,1,0,0,0 +access_sale_order_line_seat_ticket_tms_admin,sale.order.line.seat.ticket.tms.admin,model_seat_ticket_line,tms.group_tms_admin,1,1,1,1 diff --git a/tms_sale/static/description/icon.png b/tms_sale/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..d61de6bb90cbd61a11873a9a4108efabd0cd2485 GIT binary patch literal 4277 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 X0Avpl@=u�NkvXXu0mjf(Vs+v literal 0 HcmV?d00001 diff --git a/tms_sale/static/description/icon.svg b/tms_sale/static/description/icon.svg new file mode 100644 index 000000000..4850af26e --- /dev/null +++ b/tms_sale/static/description/icon.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tms_sale/static/src/js/line_ticket_wizard_controller.js b/tms_sale/static/src/js/line_ticket_wizard_controller.js new file mode 100644 index 000000000..a3cc159a3 --- /dev/null +++ b/tms_sale/static/src/js/line_ticket_wizard_controller.js @@ -0,0 +1,31 @@ +/** @odoo-module **/ + +import {formView} from "@web/views/form/form_view"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +export class TicketConfiguratorController extends formView.Controller { + setup() { + super.setup(); + this.action = useService("action"); + } + + async onRecordSaved(record) { + await super.onRecordSaved(...arguments); + const {trip_id, ticket_ids} = record.data; + return this.action.doAction({ + type: "ir.actions.act_window_close", + infos: { + ticketConfiguration: { + tms_trip_ticket_id: trip_id, + tms_ticket_ids: ticket_ids._currentIds, + }, + }, + }); + } +} + +registry.category("views").add("sale_order_line_seat_ticket_form", { + ...formView, + Controller: TicketConfiguratorController, +}); diff --git a/tms_sale/static/src/js/line_trip_wizard_controller.js b/tms_sale/static/src/js/line_trip_wizard_controller.js new file mode 100644 index 000000000..2622a1741 --- /dev/null +++ b/tms_sale/static/src/js/line_trip_wizard_controller.js @@ -0,0 +1,35 @@ +/** @odoo-module **/ + +import {formView} from "@web/views/form/form_view"; +import {registry} from "@web/core/registry"; +import {useService} from "@web/core/utils/hooks"; + +export class TripConfiguratorController extends formView.Controller { + setup() { + super.setup(); + this.action = useService("action"); + } + + async onRecordSaved(record) { + await super.onRecordSaved(...arguments); + const {origin, destination, start, end, has_route, route} = record.data; + return this.action.doAction({ + type: "ir.actions.act_window_close", + infos: { + tripConfiguration: { + tms_origin_id: origin, + tms_destination_id: destination, + tms_scheduled_date_start: start, + tms_scheduled_date_end: end, + tms_route_flag: has_route, + tms_route_id: route, + }, + }, + }); + } +} + +registry.category("views").add("sale_order_line_trip_form", { + ...formView, + Controller: TripConfiguratorController, +}); diff --git a/tms_sale/static/src/js/sale_order_line_product_field.js b/tms_sale/static/src/js/sale_order_line_product_field.js new file mode 100644 index 000000000..56b5c9cc8 --- /dev/null +++ b/tms_sale/static/src/js/sale_order_line_product_field.js @@ -0,0 +1,150 @@ +/** @odoo-module **/ + +import {SaleOrderLineProductField} from "@sale/js/sale_product_field"; +import {patch} from "@web/core/utils/patch"; +import {x2ManyCommands} from "@web/core/orm_service"; + +function formatDateForOdoo(dateString) { + const date = new Date(dateString); + const year = date.getFullYear(); + const month = ("0" + (date.getMonth() + 1)).slice(-2); + const day = ("0" + date.getDate()).slice(-2); + const hours = ("0" + date.getHours()).slice(-2); + const minutes = ("0" + date.getMinutes()).slice(-2); + const seconds = ("0" + date.getSeconds()).slice(-2); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} + +patch(SaleOrderLineProductField.prototype, { + async _onProductUpdate() { + super._onProductUpdate(...arguments); + if (this.props.record.data.has_trip_product === true) { + this._openTripConfigurator(); + } else if (this.props.record.data.seat_ticket === true) { + this._openTicketConfigurator(); + } + }, + + _editLineConfiguration() { + super._editLineConfiguration(...arguments); + if ( + this.props.record.data.has_trip_product === true || + this.props.record.data.tms_scheduled_date_start + ) { + this._openTripConfigurator(); + } else if ( + this.props.record.data.seat_ticket === true || + this.props.record.data.tms_trip_ticket_id + ) { + this._openTicketConfigurator(); + } + }, + + get isConfigurableLine() { + return ( + super.isConfigurableLine || + this.props.record.data.has_trip_product === true || + this.props.record.data.tms_scheduled_date_start || + this.props.record.data.seat_ticket === true || + this.props.record.data.tms_trip_ticket_id + ); + }, + + async _openTripConfigurator() { + const actionContext = { + default_product_template_id: this.props.record.data.product_template_id[0], + }; + if (this.props.record.data.tms_origin_id) { + actionContext.default_origin = this.props.record.data.tms_origin_id[0]; + } + if (this.props.record.data.tms_destination_id) { + actionContext.default_destination = + this.props.record.data.tms_destination_id[0]; + } + if (this.props.record.data.tms_scheduled_date_start) { + actionContext.default_start = formatDateForOdoo( + this.props.record.data.tms_scheduled_date_start + ); + } + if (this.props.record.data.tms_scheduled_date_end) { + actionContext.default_end = formatDateForOdoo( + this.props.record.data.tms_scheduled_date_end + ); + } + if (this.props.record.data.tms_route_flag) { + actionContext.default_has_route = this.props.record.data.tms_route_flag; + } + if (this.props.record.data.tms_route_id) { + actionContext.default_route = this.props.record.data.tms_route_id[0]; + } + if (this.props.record.resId) { + actionContext.default_order_line_id = this.props.record.resId; + } + + this.action.doAction("tms_sale.action_view_trip_sale_order_line", { + additionalContext: actionContext, + onClose: async (closeInfo) => { + if (!closeInfo || closeInfo.special) { + // Wizard popup closed or 'Cancel' button triggered + if ( + (!this.props.record.data.tms_origin_id && + !this.props.record.data.tms_route_flag) || + (!this.props.record.data.tms_destination_id && + !this.props.record.data.tms_route_flag) || + !this.props.record.data.tms_scheduled_date_start || + !this.props.record.data.tms_scheduled_date_end || + (!this.props.record.data.tms_route_id && + this.props.record.data.tms_route_flag) + ) { + // Remove product if trip configuration was cancelled. + this.props.record.update({ + [this.props.name]: undefined, + }); + } + } else { + const tripConfiguration = closeInfo.tripConfiguration; + this.props.record.update(tripConfiguration); + } + }, + }); + }, + + async _openTicketConfigurator() { + console.log("Data: ", this.props.record.data); + console.log("tms_ticket_ids:", typeof this.props.record.data.tms_ticket_ids); + const actionContext = { + default_product_template_id: this.props.record.data.product_template_id[0], + }; + if (this.props.record.data.tms_trip_ticket_id) { + actionContext.default_trip_id = + this.props.record.data.tms_trip_ticket_id[0]; + } + if (this.props.record.data.tms_ticket_ids) { + actionContext.default_ticket_ids = + this.props.record.data.tms_ticket_ids._currentIds; + } + if (this.props.record.resId) { + actionContext.default_order_line_id = this.props.record.resId; + } + this.action.doAction("tms_sale.action_view_seat_ticket_sale_order_line", { + additionalContext: actionContext, + onClose: async (closeInfo) => { + if (!closeInfo || closeInfo.special) { + if (!this.props.record.data.tms_trip_ticket_id) { + // Remove product if trip configuration was cancelled. + this.props.record.update({ + [this.props.name]: undefined, + }); + } + } else { + const {tms_trip_ticket_id, tms_ticket_ids} = + closeInfo.ticketConfiguration; + this.props.record.update({ + tms_trip_ticket_id, + tms_ticket_ids: [x2ManyCommands.set(tms_ticket_ids)], + }); + } + }, + }); + }, +}); diff --git a/tms_sale/views/product_template_views.xml b/tms_sale/views/product_template_views.xml new file mode 100644 index 000000000..2a1c43d17 --- /dev/null +++ b/tms_sale/views/product_template_views.xml @@ -0,0 +1,36 @@ + + + product.template.common.form.inherit.tms.product + product.template + + + + + + + + + + + + diff --git a/tms_sale/views/sale_order_views.xml b/tms_sale/views/sale_order_views.xml new file mode 100644 index 000000000..3d8ee6a5a --- /dev/null +++ b/tms_sale/views/sale_order_views.xml @@ -0,0 +1,40 @@ + + + sale.order.view.form.tms.sale + sale.order + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tms_sale/views/tms_order_views.xml b/tms_sale/views/tms_order_views.xml new file mode 100644 index 000000000..a72c5a990 --- /dev/null +++ b/tms_sale/views/tms_order_views.xml @@ -0,0 +1,41 @@ + + + + tms.order.view.form.inherit.tms.sale + tms.order + + + + + + + + + + + + + + + + + + + + + diff --git a/tms_sale/wizard/__init__.py b/tms_sale/wizard/__init__.py new file mode 100644 index 000000000..467abd143 --- /dev/null +++ b/tms_sale/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import sale_order_line_trip +from . import seat_ticket_line diff --git a/tms_sale/wizard/sale_order_line_trip.py b/tms_sale/wizard/sale_order_line_trip.py new file mode 100644 index 000000000..28b04418b --- /dev/null +++ b/tms_sale/wizard/sale_order_line_trip.py @@ -0,0 +1,31 @@ +from odoo import api, fields, models + + +class SaleOrderLineTrip(models.TransientModel): + _name = "sale.order.line.trip" + order_line_id = fields.Many2one("sale.order.line") + + has_route = fields.Boolean(string="Use Routes") + route = fields.Many2one("tms.route") + origin = fields.Many2one( + "res.partner", + domain="[('tms_location', '=', 'True')]", + context={"default_tms_location": True}, + ) + destination = fields.Many2one( + "res.partner", + domain="[('tms_location', '=', 'True')]", + context={"default_tms_location": True}, + ) + start = fields.Datetime(string="Scheduled start") + end = fields.Datetime(string="Scheduled end") + + order_confirmed = fields.Boolean(readonly=True) + + @api.onchange("origin", "destination", "start", "end", "has_route", "route") + def _compute_readonly_fields(self): + state = self.order_line_id.order_id.state + if state == "sale" or state == "cancelled": + self.order_confirmed = True + else: + self.order_confirmed = False diff --git a/tms_sale/wizard/sale_order_line_trip_views.xml b/tms_sale/wizard/sale_order_line_trip_views.xml new file mode 100644 index 000000000..258fc00ab --- /dev/null +++ b/tms_sale/wizard/sale_order_line_trip_views.xml @@ -0,0 +1,90 @@ + + + trip.sale.order.line.trip.view.form + sale.order.line.trip + + +
+ + + + + + + +
+ +
+
+ + + + + + +
+ +
+
+
+ + + + + + +
+ +
+
+ +
+ + + + + + +
+ +
+
+
+
+
+ +
+
+ + + Trip information + sale.order.line.trip + form + new + + +
diff --git a/tms_sale/wizard/seat_ticket_line.py b/tms_sale/wizard/seat_ticket_line.py new file mode 100644 index 000000000..30f93349d --- /dev/null +++ b/tms_sale/wizard/seat_ticket_line.py @@ -0,0 +1,21 @@ +from odoo import api, fields, models + + +class SeatTicketLine(models.TransientModel): + _name = "seat.ticket.line" + + order_line_id = fields.Many2one("sale.order.line") + trip_id = fields.Many2one("tms.order", options={"no_create": True}) + ticket_ids = fields.Many2many( + "seat.ticket", + options={"no_create": True}, + ) + order_confirmed = fields.Boolean(readonly=True) + + @api.onchange("trip_id") + def _compute_readonly_fields(self): + state = self.order_line_id.order_id.state + if state == "sale" or state == "cancelled": + self.order_confirmed = True + else: + self.order_confirmed = False diff --git a/tms_sale/wizard/seat_ticket_line_views.xml b/tms_sale/wizard/seat_ticket_line_views.xml new file mode 100644 index 000000000..2e5c13e90 --- /dev/null +++ b/tms_sale/wizard/seat_ticket_line_views.xml @@ -0,0 +1,42 @@ + + + trip.sale.order.line.seat.ticket.view.form + seat.ticket.line + + +
+ + + + + +
+
+ +
+
+ + + Ticket for trip: + seat.ticket.line + form + new + + +