diff --git a/.eslintrc.yml b/.eslintrc.yml index fed88d70..a61c67db 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 00000000..38929e87 --- /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 00000000..9b429614 --- /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 00000000..da313aae --- /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 00000000..2fad64e9 --- /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 00000000..770fb62c --- /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 00000000..73955264 --- /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 00000000..030a3d6a --- /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 00000000..42ed4bf3 --- /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 00000000..4231d0cc --- /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 00000000..ff064c3d --- /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 00000000..d61de6bb Binary files /dev/null and b/tms_sale/static/description/icon.png differ diff --git a/tms_sale/static/description/icon.svg b/tms_sale/static/description/icon.svg new file mode 100644 index 00000000..4850af26 --- /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 00000000..a3cc159a --- /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 00000000..2622a174 --- /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 00000000..56b5c9cc --- /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 00000000..2a1c43d1 --- /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 00000000..3d8ee6a5 --- /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 00000000..a72c5a99 --- /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 00000000..467abd14 --- /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 00000000..28b04418 --- /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 00000000..258fc00a --- /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 00000000..30f93349 --- /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 00000000..2e5c13e9 --- /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 + + +