diff --git a/.vscode/launch.json b/.vscode/launch.json index af9df4a..874d31d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,20 +2,39 @@ "version": "0.2.0", "configurations": [ { - "name": "odoo-dev-16.0", + "name": "odoo-dev", "type": "python", "request": "launch", - "cwd":"${workspaceRoot}", + "cwd": "${workspaceRoot}", "program": "${config:odoo.path}/odoo-bin", "console": "integratedTerminal", "args": [ - "--database=16-nfu", + "--database=nfu_sale_order_batch", "--addons-path=${config:odoo.addons_path},${workspaceFolder}", "--limit-time-real=0", "--limit-time-cpu=0", -// "--init=", -// "--update=", + "--init=sale_order_batch", + // "--update=", "--dev=xml" ] - } ] -} \ No newline at end of file + }, + { + "name": "odoo-scratch", + "type": "python", + "request": "launch", + "cwd": "${workspaceRoot}", + "program": "${config:odoo.path}/odoo-bin", + "console": "integratedTerminal", + "args": [ + "--database=odoo-scratch", + "--http-port=9069", + "--addons-path=${config:odoo.addons_path}", + "--limit-time-real=0", + "--limit-time-cpu=0", + "--init=sale_order_batch", + // "--update=", + "--dev=xml" + ] + } + ] +} diff --git a/.vscode/nfu.code-workspace b/.vscode/nfu.code-workspace index 7e8d7a8..b57e4a6 100644 --- a/.vscode/nfu.code-workspace +++ b/.vscode/nfu.code-workspace @@ -7,5 +7,7 @@ "path": "../../../versions/16.0" } ], - "settings": {} -} \ No newline at end of file + "settings": { + "python.languageServer": "None" + } +} diff --git a/.vscode/settings.json b/.vscode/settings.json index bfb137d..ed0d60f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "odoo.version": "16.0", - "odoo.path": "${workspaceFolder}/../../versions/${config:odoo.version}/odoo", + "odoo.path": "../../versions/${config:odoo.version}/odoo", "odoo.addons_path": "${config:odoo.path}/addons", -} \ No newline at end of file + "python.analysis.extraPaths": ["../../versions/16.0/odoo", "../../versions/16.0/OCA"] +} diff --git a/sale_order_batch/README.rst b/sale_order_batch/README.rst new file mode 100644 index 0000000..4eb7562 --- /dev/null +++ b/sale_order_batch/README.rst @@ -0,0 +1,67 @@ +================ +Sale Order Batch +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:6686f0672ede8093b17d5d7b165f72f8e49341c7849f9e0d5a162e350fc36d71 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png + :target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html + :alt: License: LGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-bruecksen%2Fife_nfu-lightgray.png?logo=github + :target: https://github.com/bruecksen/ife_nfu/tree/16.0/sale_order_batch + :alt: bruecksen/ife_nfu + +|badge1| |badge2| |badge3| + +This module adds the possibility to group Sale orders into batches. Attributes of a Sale Order Batch will be written to all +Sale Order. Wenn attached to a batch they can be processed together. On the other hand they cannot be proccesed individual +anymore as log as they are part of a batch. Only Sale Orders in state Qutotation or Quotation Send can be attached to a batch. + +**Table of contents** + +.. contents:: + :local: + +Changelog +========= + +:0.0.1: Initial module. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* BAKEUP + +Contributors +~~~~~~~~~~~~ + +* Niels Göttsch + +Maintainers +~~~~~~~~~~~ + +This module is part of the `bruecksen/ife_nfu `_ project on GitHub. + +You are welcome to contribute. diff --git a/sale_order_batch/__init__.py b/sale_order_batch/__init__.py new file mode 100644 index 0000000..0650744 --- /dev/null +++ b/sale_order_batch/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_order_batch/__manifest__.py b/sale_order_batch/__manifest__.py new file mode 100644 index 0000000..2c1b66b --- /dev/null +++ b/sale_order_batch/__manifest__.py @@ -0,0 +1,17 @@ +{ + "name": "Sale Order Batch", + "summary": "Group Sale Orders into a batch", + "author": "BAKEUP", + "website": "https://www.bakeup.org", + "category": "Sale", + "version": "16.0.0.0.1", + "depends": ["sale", "product"], + "data": [ + "data/ir_sequence_data.xml", + "views/sale_order_batch_views.xml", + "views/sale_order_views.xml", + "views/sale_menus.xml", + "security/ir.model.access.csv", + ], + "license": "LGPL-3", +} diff --git a/sale_order_batch/data/ir_sequence_data.xml b/sale_order_batch/data/ir_sequence_data.xml new file mode 100644 index 0000000..4c14e3e --- /dev/null +++ b/sale_order_batch/data/ir_sequence_data.xml @@ -0,0 +1,12 @@ + + + + + Sales Order Batch + sale.order.batch + SOB + 5 + + + + diff --git a/sale_order_batch/models/__init__.py b/sale_order_batch/models/__init__.py new file mode 100644 index 0000000..03035b7 --- /dev/null +++ b/sale_order_batch/models/__init__.py @@ -0,0 +1,5 @@ +from . import product_product +from . import sale_order +from . import sale_order_batch +from . import sale_order_batch_product +from . import sale_order_line diff --git a/sale_order_batch/models/product_product.py b/sale_order_batch/models/product_product.py new file mode 100644 index 0000000..37735c0 --- /dev/null +++ b/sale_order_batch/models/product_product.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ProductProduct(models.Model): + _inherit = "product.product" + + batch_ids = fields.Many2many("sale.order.batch", "product_ids") diff --git a/sale_order_batch/models/sale_order.py b/sale_order_batch/models/sale_order.py new file mode 100644 index 0000000..1fdf8bc --- /dev/null +++ b/sale_order_batch/models/sale_order.py @@ -0,0 +1,53 @@ +from odoo import _, fields, models +from odoo.exceptions import UserError + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + batch_id = fields.Many2one("sale.order.batch", copy=False) + + def action_add_to_batch(self): + """ + TODO: Add Wizard to choose between batches if more than one existis. Picking the first one for now. + """ + batch = self.env["sale.order.batch"].search([("state", "=", "open")], limit=1) + if not batch: + batch = self.env["sale.order.batch"].create({}) + for sale_order in self: + if not sale_order.batch_id and sale_order.state in ["draft", "sent"]: + sale_order.batch_id = batch + + def action_view_sale_order_batch(self): + return { + "name": _("Sale Order Batch"), + "type": "ir.actions.act_window", + "view_type": "form", + "view_mode": "form", + "res_model": "sale.order.batch", + "res_id": self.batch_id.id, + # 'target': '', + } + + def action_confirm(self): + invalid_orders = [] + if not self.env.context.get("bypass_batch", False): + for order in self: + if order.batch_id: + invalid_orders.append(order.name) + if invalid_orders: + raise UserError(_(f"Sale Order belongs to a Batch: {', '.join(invalid_orders)}")) + return super().action_confirm() + + def write(self, vals): + if "batch_id" in vals: + invalid_orders = [] + for order in self: + if order.state not in ["draft", "sent"]: + invalid_orders.append(order.name) + if invalid_orders: + raise UserError(_(f"Sale Order not in State Draft or Sent: {', '.join(invalid_orders)}")) + res = super().write(vals) + if "batch_id" in vals: + self.order_line._update_batch_products() + return res diff --git a/sale_order_batch/models/sale_order_batch.py b/sale_order_batch/models/sale_order_batch.py new file mode 100644 index 0000000..59afb0a --- /dev/null +++ b/sale_order_batch/models/sale_order_batch.py @@ -0,0 +1,137 @@ +from odoo import _, api, fields, models + + +STATES = [("open", "Open"), ("close", "Close")] + + +class SaleOrderBatch(models.Model): + """Group serveral Sale Orders into a batch""" + + _name = "sale.order.batch" + _description = "Sale Order Batch" + _inherit = "mail.thread" + _order = "date_order desc, id desc" + + # TODO: add company_id to sale order batch + # company_id = fields.Many2one( + # related='order_id.company_id', + # store=True, index=True, precompute=True) + + name = fields.Char( + string="Order Batch Reference", + required=True, + copy=False, + readonly=True, + index="trigram", + default=lambda self: _("New"), + ) + state = fields.Selection( + selection=[("open", "Open"), ("closed", "Closed")], + string="Status", + readonly=True, + copy=False, + index=True, + tracking=1, + default="open", + ) + date_order = fields.Datetime( + string="Order Date", + required=True, + readonly=False, + copy=False, + states={"closed": [("readonly", True)]}, + help="Creation date of order batch,\nConfirmation date of confirmed orders.", + default=fields.Datetime.now, + ) + validity_date = fields.Date(compute="_compute_validity_date") + sale_order_ids = fields.One2many("sale.order", "batch_id") + sale_order_count = fields.Integer(compute="_compute_sale_order_count") + sale_order_line_ids = fields.Many2many("sale.order.line", compute="_compute_sale_order_line_ids", store=True) + amount_total = fields.Float(compute="_compute_amount_total", string="Total") + product_ids = fields.One2many("sale.order.batch.product", "batch_id") + product_count = fields.Integer(compute="_compute_product_count") + partner_credit_warning = fields.Text(compute="_compute_partner_credit_warning") + + @api.depends("sale_order_ids.validity_date") + def _compute_validity_date(self): + for batch in self: + if batch.sale_order_ids: + batch.validity_date = min(batch.sale_order_ids.mapped("validity_date")) + else: + batch.validity_date = False + + @api.depends("sale_order_ids") + def _compute_sale_order_count(self): + for batch in self: + batch.sale_order_count = len(batch.sale_order_ids) + + @api.depends("sale_order_ids.order_line") + def _compute_sale_order_line_ids(self): + for batch in self: + order_lines = self.env["sale.order.line"].search([("order_id", "in", batch.sale_order_ids.ids)]) + batch.sale_order_line_ids = order_lines + + @api.depends("sale_order_ids.amount_total") + def _compute_amount_total(self): + for batch in self: + batch.amount_total = sum(batch.sale_order_ids.mapped("amount_total")) + + @api.depends("product_ids") + def _compute_product_count(self): + for batch in self: + batch.product_count = len(batch.product_ids) + + @api.depends("sale_order_ids.partner_credit_warning") + def _compute_partner_credit_warning(self): + for batch in self: + batch.partner_credit_warning = "" + for order in batch.sale_order_ids: + if order.partner_credit_warning: + batch.partner_credit_warning += order.partner_credit_warning + "\n" + + def action_view_source_sale_orders(self): + self.ensure_one() + result = self.env["ir.actions.act_window"]._for_xml_id("sale.action_orders") + if len(self.sale_order_ids) > 1: + result["domain"] = [("id", "in", self.sale_order_ids.ids)] + elif len(self.sale_order_ids) == 1: + result["views"] = [(self.env.ref("sale.view_order_form", False).id, "form")] + result["res_id"] = self.sale_order_ids.id + else: + result = {"type": "ir.actions.act_window_close"} + return result + + def action_view_products(self): + self.ensure_one() + result = self.env["ir.actions.act_window"]._for_xml_id("product.product_normal_action_sell") + if len(self.product_ids) > 1: + result["domain"] = [("id", "in", self.product_ids.ids)] + elif len(self.product_ids) == 1: + result["views"] = [(self.env.ref("product.product_product_tree_view", False).id, "form")] + result["res_id"] = self.product_ids.id + else: + result = {"type": "ir.actions.act_window_close"} + return result + + def action_confirm(self): + for batch in self: + orders = batch.with_context(bypass_batch=True).sale_order_ids + orders.action_confirm() + batch.state = "closed" + return True + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + if "company_id" in vals: + self = self.with_company(vals["company_id"]) + if vals.get("name", _("New")) == _("New"): + seq_date = ( + fields.Datetime.context_timestamp(self, fields.Datetime.to_datetime(vals["date_order"])) + if "date_order" in vals + else None + ) + vals["name"] = self.env["ir.sequence"].next_by_code("sale.order.batch", sequence_date=seq_date) or _( + "New" + ) + return super().create(vals_list) diff --git a/sale_order_batch/models/sale_order_batch_product.py b/sale_order_batch/models/sale_order_batch_product.py new file mode 100644 index 0000000..60a7d86 --- /dev/null +++ b/sale_order_batch/models/sale_order_batch_product.py @@ -0,0 +1,46 @@ +from odoo import api, fields, models + + +class SaleOrderBatchProduct(models.Model): + """Show Products available in Sale Order Batch""" + + _name = "sale.order.batch.product" + _description = "Sale Order Batch Product" + + batch_id = fields.Many2one( + comodel_name="sale.order.batch", + string="Batch Reference", + required=True, + ondelete="cascade", + index=True, + copy=False, + ) + + product_id = fields.Many2one(comodel_name="product.product", required=True, readonly=False) + + product_template_id = fields.Many2one( + "product.template", related="product_id.product_tmpl_id", string="Product Template" + ) + sale_order_line_ids = fields.Many2many("sale.order.line", compute="_compute_sale_order_line_ids") + product_uom_category_id = fields.Many2one(related="product_id.uom_id.category_id", depends=["product_id"]) + product_uom_qty = fields.Float(compute="_compute_uom_qty") + product_uom = fields.Many2one(related="product_id.uom_id") + product_packaging_id = fields.Many2one("product.packaging") + product_packaging_qty = fields.Float(compute="_compute_product_packaging_qty") + + @api.depends("batch_id.sale_order_line_ids") + def _compute_sale_order_line_ids(self): + for product in self: + product.sale_order_line_ids = product.batch_id.sale_order_line_ids.filtered( + lambda o: o.product_id == product.product_id + ) + + @api.depends("batch_id.sale_order_line_ids") + def _compute_uom_qty(self): + for product in self: + product.product_uom_qty = sum(product.sale_order_line_ids.mapped("product_uom_qty")) + + @api.depends("product_packaging_id") + def _compute_product_packaging_qty(self): + for product in self: + product.product_packaging_qty = product.product_packaging_id.qty if product.product_packaging_id else 1 diff --git a/sale_order_batch/models/sale_order_line.py b/sale_order_batch/models/sale_order_line.py new file mode 100644 index 0000000..1c0c5ae --- /dev/null +++ b/sale_order_batch/models/sale_order_line.py @@ -0,0 +1,21 @@ +from odoo import api, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + def _update_batch_products(self): + for order_line in self: + batch_id = order_line.order_id.batch_id + if batch_id: + registered_products = batch_id.mapped("product_ids").mapped("product_id") + if order_line.product_id not in registered_products: + self.env["sale.order.batch.product"].create( + {"batch_id": batch_id.id, "product_id": order_line.product_id.id} + ) + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + res._update_batch_products() + return res diff --git a/sale_order_batch/readme/CONTRIBUTORS.rst b/sale_order_batch/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000..fda28e1 --- /dev/null +++ b/sale_order_batch/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Niels Göttsch \ No newline at end of file diff --git a/sale_order_batch/readme/DESCRIPTION.rst b/sale_order_batch/readme/DESCRIPTION.rst new file mode 100644 index 0000000..35b2b44 --- /dev/null +++ b/sale_order_batch/readme/DESCRIPTION.rst @@ -0,0 +1,3 @@ +This module adds the possibility to group Sale orders into batches. Attributes of a Sale Order Batch will be written to all +Sale Order. Wenn attached to a batch they can be processed together. On the other hand they cannot be proccesed individual +anymore as log as they are part of a batch. Only Sale Orders in state Qutotation or Quotation Send can be attached to a batch. \ No newline at end of file diff --git a/sale_order_batch/readme/HISTORY.rst b/sale_order_batch/readme/HISTORY.rst new file mode 100644 index 0000000..e986fe7 --- /dev/null +++ b/sale_order_batch/readme/HISTORY.rst @@ -0,0 +1 @@ +:0.0.1: Initial module. \ No newline at end of file diff --git a/sale_order_batch/security/ir.model.access.csv b/sale_order_batch/security/ir.model.access.csv new file mode 100644 index 0000000..899c9f1 --- /dev/null +++ b/sale_order_batch/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_order_batch,sale.order.batch,model_sale_order_batch,sales_team.group_sale_salesman,1,1,1,0 +access_sale_order_batch_manager,sale.order.batch.manager,model_sale_order,sales_team.group_sale_manager,1,1,1,1 +access_sale_order_batch_product,sale.order.batch.product,model_sale_order_batch_product,sales_team.group_sale_salesman,1,1,1,1 diff --git a/sale_order_batch/static/description/index.html b/sale_order_batch/static/description/index.html new file mode 100644 index 0000000..79ce309 --- /dev/null +++ b/sale_order_batch/static/description/index.html @@ -0,0 +1,430 @@ + + + + + +Sale Order Batch + + + +
+

Sale Order Batch

+ + +

Beta License: LGPL-3 bruecksen/ife_nfu

+

This module adds the possibility to group Sale orders into batches. Attributes of a Sale Order Batch will be written to all +Sale Order. Wenn attached to a batch they can be processed together. On the other hand they cannot be proccesed individual +anymore as log as they are part of a batch. Only Sale Orders in state Qutotation or Quotation Send can be attached to a batch.

+

Table of contents

+ +
+

Changelog

+ +++ + + + +
0.0.1:Initial module.
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • BAKEUP
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the bruecksen/ife_nfu project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/sale_order_batch/views/sale_menus.xml b/sale_order_batch/views/sale_menus.xml new file mode 100644 index 0000000..82eca0b --- /dev/null +++ b/sale_order_batch/views/sale_menus.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/sale_order_batch/views/sale_order_batch_views.xml b/sale_order_batch/views/sale_order_batch_views.xml new file mode 100644 index 0000000..2eb1ecc --- /dev/null +++ b/sale_order_batch/views/sale_order_batch_views.xml @@ -0,0 +1,145 @@ + + + + sale.order.batch.tree + sale.order.batch + + + + + + + + + + + + + sale.order.batch.form + sale.order.batch + +
+
+
+ + +
+ + +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + sale.order.batch.view.search + sale.order.batch + + + + + + + + + + Sale Order Batches + ir.actions.act_window + sale.order.batch + tree,form + {'search_default_filter_batch_open': 1} + +
diff --git a/sale_order_batch/views/sale_order_views.xml b/sale_order_batch/views/sale_order_views.xml new file mode 100644 index 0000000..04be557 --- /dev/null +++ b/sale_order_batch/views/sale_order_views.xml @@ -0,0 +1,54 @@ + + + + sale.order.form.inherit.batch + sale.order + + + + + + + + {'invisible': ['|',('batch_id','!=',False),('state', 'not in', ['sent'])]} + + + {'invisible': ['|',('batch_id','!=',False),('state', 'not in', ['draft'])]} + + + + + + sale.order.list.view.inherited + sale.order + + + +
+
+
+
+
+