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..f7a55dc5 --- /dev/null +++ b/tms_sale/__manifest__.py @@ -0,0 +1,21 @@ +# 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"], + "data": [ + "security/ir.model.access.csv", + "views/sale_order_views.xml", + "views/tms_order_views.xml", + "views/product_template_views.xml", + ], + "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..63f2ab87 --- /dev/null +++ b/tms_sale/models/__init__.py @@ -0,0 +1,3 @@ +from . import sale_order_line +from . import sale_order +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..d54a0209 --- /dev/null +++ b/tms_sale/models/sale_order.py @@ -0,0 +1,212 @@ +# Copyright (C) 2019 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") + + tms_route_flag = fields.Boolean(string="Use Routes") + tms_route_id = fields.Many2one("tms.route", string="Routes") + tms_origin_id = fields.Many2one( + "res.partner", + string="Origin", + domain="[('tms_type', '=', 'location')]", + context={"default_tms_type": "location"}, + ) + tms_destination_id = fields.Many2one( + "res.partner", + string="Destination", + domain="[('tms_type', '=', 'location')]", + context={"default_tms_type": "location"}, + ) + + tms_scheduled_date_start = fields.Datetime(string="Scheduled Start") + tms_scheduled_date_end = fields.Datetime(string="Scheduled End") + + @api.onchange("order_line") + def _onchange_tms_product_uom(self): + for line in self.order_line: + if line.product_template_id.tms_factor_distance_uom: + line.tms_factor_uom = ( + line.product_template_id.tms_factor_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 + + @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 _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.id, sol_id=line.id) + return vals + + 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 + 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 _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 qty in range(int(line.product_uom_qty) - len(line.tms_order_ids)): + vals = self._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 + + 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 + + @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): + order = super().write(vals) + if "order_line" in vals and self.has_tms_order: + self._action_create_new_trips() + return order diff --git a/tms_sale/models/sale_order_line.py b/tms_sale/models/sale_order_line.py new file mode 100644 index 00000000..f92d325d --- /dev/null +++ b/tms_sale/models/sale_order_line.py @@ -0,0 +1,70 @@ +# 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 SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + 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() + tms_factor = fields.Float(default=1) + + tms_route_id = fields.Many2one("tms.route", string="Routes") + tms_origin_id = fields.Many2one( + "res.partner", + string="Origin", + domain="[('tms_type', '=', 'location')]", + context={"default_tms_type": "location"}, + ) + tms_destination_id = fields.Many2one( + "res.partner", + string="Destination", + domain="[('tms_type', '=', 'location')]", + context={"default_tms_type": "location"}, + ) + + tms_scheduled_date_start = fields.Datetime(string="Scheduled Start") + tms_scheduled_date_end = fields.Datetime(string="Scheduled End") + + 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", + "context": {"default_user_id": self.id}, + } + + 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() diff --git a/tms_sale/models/tms_order.py b/tms_sale/models/tms_order.py new file mode 100644 index 00000000..44e65959 --- /dev/null +++ b/tms_sale/models/tms_order.py @@ -0,0 +1,32 @@ +# 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) + + 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 + 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..6e0af481 --- /dev/null +++ b/tms_sale/security/ir.model.access.csv @@ -0,0 +1,2 @@ +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 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/views/product_template_views.xml b/tms_sale/views/product_template_views.xml new file mode 100644 index 00000000..531200e1 --- /dev/null +++ b/tms_sale/views/product_template_views.xml @@ -0,0 +1,38 @@ + + + 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..18b97bc9 --- /dev/null +++ b/tms_sale/views/sale_order_views.xml @@ -0,0 +1,101 @@ + + + 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..3f93fa72 --- /dev/null +++ b/tms_sale/views/tms_order_views.xml @@ -0,0 +1,24 @@ + + + + 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..7c001433 --- /dev/null +++ b/tms_sale/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_order_line_trip 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..35184ffc --- /dev/null +++ b/tms_sale/wizard/sale_order_line_trip.py @@ -0,0 +1,14 @@ +from odoo import fields, models + + +class SaleOrdeLineTrip(models.TransientModel): + _name = "sale.order.line.trip" + order_line_id = fields.Many2one("sale.order.line") + + route = fields.Many2one("tms.route", related="order_line_id.tms_route_id") + origin = fields.Many2one("res.partner", related="order_line_id.tms_origin_id") + destination = fields.Many2one( + "res.partner", related="order_line_id.tms_destination_id" + ) + start = fields.Datetime(related="order_line_id.tms_scheduled_date_start") + end = fields.Datetime(related="order_line_id.tms_scheduled_date_end") 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..8f02e8f9 --- /dev/null +++ b/tms_sale/wizard/sale_order_line_trip_views.xml @@ -0,0 +1,37 @@ + + + sale.order.line.view.inherit.form + sale.order.line + + + +
+
+
+
+
+ + + trip.sale.order.line.trip.view.form + sale.order.line.trip + + +
+ + + + + + + + +
+
+
+