diff --git a/sale_order_batch/README.rst b/sale_order_batch/README.rst new file mode 100644 index 0000000..66e750a --- /dev/null +++ b/sale_order_batch/README.rst @@ -0,0 +1,82 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +================ +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/license-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-madmooose%2Fodooapps-lightgray.png?logo=github + :target: https://github.com/madmooose/odooapps/tree/16.0/sale_order_batch + :alt: madmooose/odooapps + +|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 +========= + +- 16.0.0.0.1: Initial module. +- 16.0.0.1: Add Multi Company ability +- 16.0.0.2: rename product creation methode +- 16.0.0.3: Make validity_date persistent, add batches to SO views +- 16.0.0.4: Remove validity date +- 16.0.0.5: Move product logic to sale order lines +- 16.0.2.1.0: Add "In Progress" and "Cancelled" state +- 16.0.2.2.0: Add invoice status and menues +- 16.0.2.2.1: Add lst_price to view +- 16.0.2.3.0: Add "Reset to Open" +- 16.0.2.3.1: Remove Sale Order from batch on cancelation + +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 +* Niels Göttsch + +Contributors +~~~~~~~~~~~~ + +* Niels Göttsch + +Maintainers +~~~~~~~~~~~ + +This module is part of the `madmooose/odooapps `_ 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..9b42961 --- /dev/null +++ b/sale_order_batch/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sale_order_batch/__manifest__.py b/sale_order_batch/__manifest__.py new file mode 100644 index 0000000..e57fe86 --- /dev/null +++ b/sale_order_batch/__manifest__.py @@ -0,0 +1,20 @@ +{ + "name": "Sale Order Batch", + "summary": "Group Sale Orders into a batch", + "author": "BAKEUP,Niels Göttsch", + "website": "https://ziemlichoptimal.de", + "category": "Sale", + "version": "16.0.2.3.1", + "depends": ["sale", "product"], + "data": [ + "security/ir.model.access.csv", + "security/ir_rules.xml", + "views/sale_order_batch_views.xml", + "views/sale_order_batch_product_views.xml", + "views/sale_order_views.xml", + "views/sale_menus.xml", + "wizard/sale_order_batch_cancel_wizard.xml", + "data/ir_sequence_data.xml", + ], + "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..3e5b0eb --- /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..53efcfd --- /dev/null +++ b/sale_order_batch/models/__init__.py @@ -0,0 +1,4 @@ +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/sale_order.py b/sale_order_batch/models/sale_order.py new file mode 100644 index 0000000..69a3c7b --- /dev/null +++ b/sale_order_batch/models/sale_order.py @@ -0,0 +1,75 @@ +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, + check_company=True, + domain="[('state','!=','closed')]", + ) + batch_state = fields.Selection( + related="batch_id.state", + string="Batch State", + ) + + def _get_current_batch(self): + """ + Returns the current batch of the sale order. + :return: sale.order.batch + """ + self.ensure_one() + return self.env["sale.order.batch"].search( + [("state", "=", "open"), ("company_id", "=", self.company_id.id)], limit=1 + ) + + def action_add_to_batch(self): + """ + TODO: Add Wizard to choose between batches if more than one existis. + Picking the first one for now. + """ + for sale_order in self: + company = sale_order.company_id + sale_order = sale_order.with_company(company) + if not sale_order.batch_id and sale_order.state in ["draft", "sent"]: + batch = sale_order._get_current_batch() + if not batch: + batch = sale_order.env["sale.order.batch"].create({}) + 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, + } + + 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 action_cancel(self): + self.write({"batch_id": False}) + res = super().action_cancel() + return res + + def write(self, vals): + res = super().write(vals) + if "batch_id" in vals.keys(): + for order in self: + order.order_line._update_batch_product() + 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..0f44883 --- /dev/null +++ b/sale_order_batch/models/sale_order_batch.py @@ -0,0 +1,264 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +from odoo.addons.sale.models.sale_order import INVOICE_STATUS + +STATES = [ + ("open", "Open"), + ("in_progress", "In Progress"), + ("closed", "Closed"), + ("cancel", "Cancelled"), +] +READONLY_FIELD_STATES = {state: [("readonly", True)] for state in {"closed"}} + + +class SaleOrderBatch(models.Model): + """Group serveral Sale Orders into a batch""" + + _name = "sale.order.batch" + _description = "Sales Order Batch" + _inherit = ["mail.thread", "mail.activity.mixin"] + _order = "date_order desc, id desc" + _check_company_auto = True + + name = fields.Char( + string="Order Batch Reference", + required=True, + copy=False, + readonly=True, + index="trigram", + default=lambda self: _("New"), + ) + company_id = fields.Many2one( + comodel_name="res.company", + required=True, + index=True, + default=lambda self: self.env.company, + ) + state = fields.Selection( + selection=STATES, + 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=READONLY_FIELD_STATES, + help="Creation date of order batch,\nConfirmation date of confirmed orders.", + default=fields.Datetime.now, + ) + sale_order_ids = fields.One2many("sale.order", "batch_id") + sale_order_count = fields.Integer(compute="_compute_sale_order_count") + sale_order_line_ids = fields.One2many( + "sale.order.line", "batch_id", states=READONLY_FIELD_STATES + ) + invoice_ids = fields.Many2many("account.move", compute="_compute_invoice_ids") + invoice_count = fields.Integer(compute="_compute_invoice_ids") + invoice_status = fields.Selection( + selection=INVOICE_STATUS, compute="_compute_invoice_status", 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") + def _compute_sale_order_count(self): + for batch in self: + batch.sale_order_count = len(batch.sale_order_ids) + + @api.depends("sale_order_ids.invoice_ids") + def _compute_invoice_ids(self): + for batch in self: + invoices = batch.sale_order_ids.mapped("invoice_ids") + batch.invoice_ids = invoices + batch.invoice_count = len(invoices) + + @api.depends("sale_order_ids.invoice_status") + def _compute_invoice_status(self): + for batch in self: + if batch.sale_order_ids and any( + invoice_status == "to invoice" + for invoice_status in batch.sale_order_ids.mapped("invoice_status") + ): + batch.invoice_status = "to invoice" + elif batch.sale_order_ids and all( + invoice_status == "invoiced" + for invoice_status in batch.sale_order_ids.mapped("invoice_status") + ): + batch.invoice_status = "invoiced" + elif batch.sale_order_ids and all( + invoice_status in ("invoiced", "upselling") + for invoice_status in batch.sale_order_ids.mapped("invoice_status") + ): + batch.invoice_status = "upselling" + else: + batch.invoice_status = "no" + + @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_view_invoice(self): + invoices = self.mapped("invoice_ids") + action = self.env["ir.actions.actions"]._for_xml_id( + "account.action_move_out_invoice_type" + ) + if len(invoices) > 1: + action["domain"] = [("id", "in", invoices.ids)] + elif len(invoices) == 1: + form_view = [(self.env.ref("account.view_move_form").id, "form")] + if "views" in action: + action["views"] = form_view + [ + (state, view) for state, view in action["views"] if view != "form" + ] + else: + action["views"] = form_view + action["res_id"] = invoices.id + else: + action = {"type": "ir.actions.act_window_close"} + + action["context"] = {"default_move_type": "out_invoice"} + return action + + def action_in_progress(self): + for batch in self: + batch.sale_order_line_ids._validate_analytic_distribution() + orders = batch.sale_order_ids + orders.update({"state": "sent"}) + batch.update({"state": "in_progress"}) + return True + + def action_confirm(self): + for batch in self: + orders = batch.with_context(bypass_batch=True).sale_order_ids + orders.action_confirm() + batch.update({"state": "closed"}) + return True + + def action_cancel(self): + cancel_warning = self._show_cancel_wizard() + if cancel_warning: + self.ensure_one() + return { + "name": _("Cancel %s", self.name), + "view_mode": "form", + "type": "ir.actions.act_window", + "res_model": "sale.order.batch.cancel.wizard", + "target": "new", + "context": {"active_ids": self.ids}, + } + else: + self._action_cancel() + + def _action_cancel(self): + orders = self.mapped("sale_order_ids") + # use _action_cancel to skip the wizzard + orders._action_cancel() + self.write({"state": "cancel"}) + + def _show_cancel_wizard(self): + """Decide whether the sale.order.batch.cancel wizard should be shown to cancel + specified orders. + + :return: True if there is any non-draft order in the given orders + :rtype: bool + """ + + if self.env.context.get("disable_cancel_warning"): + return False + batch_orders = self.mapped("sale_order_ids") + return any(order._show_cancel_wizard() for order in batch_orders) + + def action_open(self): + for batch in self: + orders = batch.sale_order_ids + orders.action_draft() + batch.update({"state": "open"}) + 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"): + vals["name"] = self.env["ir.sequence"].next_by_code( + "sale.order.batch" + ) or _("New") + return super().create(vals_list) + + +class SaleOrderBatchCancelWizard(models.TransientModel): + _name = "sale.order.batch.cancel.wizard" + _description = "Sale Order Batch Cancel Wizard" + + def action_cancel(self): + batches = self.env["sale.order.batch"].browse(self._context["active_ids"]) + for batch in batches: + orders = batch.sale_order_ids + # Warn if any sale order is not in draft + not_draft_orders = orders.filtered(lambda o: o.state != "draft") + if not_draft_orders: + raise UserError( + _( + "You can only cancel batches where all sale orders are in " + "draft state." + ) + ) + # use _action_cancel to skip the wizzard + # ToDo: Implement wizard logic + orders._action_cancel() + batch.update({"state": "cancel"}) + return True + + def action_discard(self): + return {"type": "ir.actions.act_window_close"} 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..5a78e45 --- /dev/null +++ b/sale_order_batch/models/sale_order_batch_product.py @@ -0,0 +1,103 @@ +from odoo import api, fields, models + +from .sale_order_batch import READONLY_FIELD_STATES + + +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, + readonly=True, + ) + state = fields.Selection(related="batch_id.state") + product_id = fields.Many2one( + comodel_name="product.product", required=True, readonly=True + ) + product_template_id = fields.Many2one( + "product.template", + related="product_id.product_tmpl_id", + string="Product Template", + ) + sale_order_line_ids = fields.One2many( + "sale.order.line", "batch_product_id", states=READONLY_FIELD_STATES + ) + product_uom_category_id = fields.Many2one( + related="product_id.uom_id.category_id", depends=["product_id"] + ) + product_uom_qty = fields.Float( + string="Quantity", + compute="_compute_uom_qty", + precompute=True, + store=True, + digits=[12, 3], + ) + product_uom = fields.Many2one(related="product_id.uom_id") + lst_price = fields.Float( + "Sales Price", + related="product_id.lst_price", + digits="Product Price", + help="""The sale price is managed from the product template. Click on the + 'Configure Variants' button to set the extra attribute prices.""", + ) + product_packaging_id = fields.Many2one( + comodel_name="product.packaging", + string="Packaging", + compute="_compute_product_packaging_id", + store=True, + readonly=False, + precompute=True, + domain="[('sales', '=', True), ('product_id','=',product_id)]", + ) + product_packaging_qty = fields.Float(compute="_compute_product_packaging_qty") + + @api.constrains("product_id", "sale_order_line_ids") + def _check_sale_order_line_products(self): + for product in self: + if any( + line.product_id != product.product_id + for line in product.sale_order_line_ids + ): + raise ValueError("Product must be the same for all Sale Order Lines") + + @api.depends("sale_order_line_ids.product_uom_qty") + 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_id", "product_uom_qty", "product_uom") + def _compute_product_packaging_id(self): + for line in self: + # remove packaging if not match the product + if ( + line.product_packaging_id + and line.product_packaging_id.product_id != line.product_id + ): + line.product_packaging_id = False + # suggest always the first packaging + if line.product_id and line.product_uom_qty and line.product_uom: + suggested_packaging = ( + line.product_id.packaging_ids[0] + if line.product_id.packaging_ids + else False + ) + line.product_packaging_id = ( + suggested_packaging or line.product_packaging_id + ) + + @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..2bc10a4 --- /dev/null +++ b/sale_order_batch/models/sale_order_line.py @@ -0,0 +1,57 @@ +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + batch_id = fields.Many2one(related="order_id.batch_id") + batch_product_id = fields.Many2one("sale.order.batch.product", ondelete="restrict") + + def _link_batch_product(self): + lines_with_batch = self.filtered(lambda line: line.batch_id) + for line in lines_with_batch: + batch_id = line.batch_id + batch_product = self.env["sale.order.batch.product"].search( + [ + ("product_id", "=", line.product_id.id), + ("batch_id", "=", batch_id.id), + ], + limit=1, + ) + if not batch_product: + batch_product = self.env["sale.order.batch.product"].create( + {"batch_id": batch_id.id, "product_id": line.product_id.id} + ) + line.batch_product_id = batch_product + + def _unlink_batch_product(self): + lines_to_unlink = self.filtered(lambda line: line.batch_product_id) + batch_products = lines_to_unlink.mapped("batch_product_id") + lines_to_unlink.update({"batch_product_id": False}) + batch_products.filtered(lambda p: len(p.sale_order_line_ids) == 0).unlink() + + def _update_batch_product(self): + lines_with_batch = self.filtered(lambda line: line.batch_id) + lines_without_batch = self - lines_with_batch + lines_with_batch.filtered( + lambda line: line.batch_product_id + and line.batch_product_id.product_id != line.product_id + )._unlink_batch_product() + lines_with_batch._link_batch_product() + lines_without_batch._unlink_batch_product() + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + res._link_batch_product() + return res + + def write(self, vals): + res = super().write(vals) + if vals.get("product_id"): + self._update_batch_product() + return res + + def unlink(self): + self._unlink_batch_product() + return super().unlink() diff --git a/sale_order_batch/pyproject.toml b/sale_order_batch/pyproject.toml new file mode 100644 index 0000000..4231d0c --- /dev/null +++ b/sale_order_batch/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" 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..592dc2e --- /dev/null +++ b/sale_order_batch/readme/HISTORY.rst @@ -0,0 +1,11 @@ +- 16.0.0.0.1: Initial module. +- 16.0.0.1: Add Multi Company ability +- 16.0.0.2: rename product creation methode +- 16.0.0.3: Make validity_date persistent, add batches to SO views +- 16.0.0.4: Remove validity date +- 16.0.0.5: Move product logic to sale order lines +- 16.0.2.1.0: Add "In Progress" and "Cancelled" state +- 16.0.2.2.0: Add invoice status and menues +- 16.0.2.2.1: Add lst_price to view +- 16.0.2.3.0: Add "Reset to Open" +- 16.0.2.3.1: Remove Sale Order from batch on cancelation \ 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..14868b2 --- /dev/null +++ b/sale_order_batch/security/ir.model.access.csv @@ -0,0 +1,6 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_order_batch_invoicing_payments,sale.order.batch,model_sale_order_batch,account.group_account_invoice,1,1,0,0 +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_batch,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 +sale_order_batch.access_sale_order_batch_cancel_wizard,access_sale_order_batch_cancel_wizard,sale_order_batch.model_sale_order_batch_cancel_wizard,base.group_user,1,1,1,1 diff --git a/sale_order_batch/security/ir_rules.xml b/sale_order_batch/security/ir_rules.xml new file mode 100644 index 0000000..b328a0e --- /dev/null +++ b/sale_order_batch/security/ir_rules.xml @@ -0,0 +1,11 @@ + + + + + + Sales Order Batch multi-company + + [('company_id', 'in', company_ids)] + + + diff --git a/sale_order_batch/static/description/icon.png b/sale_order_batch/static/description/icon.png new file mode 100644 index 0000000..1267712 Binary files /dev/null and b/sale_order_batch/static/description/icon.png differ diff --git a/sale_order_batch/static/description/index.html b/sale_order_batch/static/description/index.html new file mode 100644 index 0000000..bd90800 --- /dev/null +++ b/sale_order_batch/static/description/index.html @@ -0,0 +1,442 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Sale Order Batch

+ +

Beta License: LGPL-3 madmooose/odooapps

+

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

+
    +
  • 16.0.0.0.1: Initial module.
  • +
  • 16.0.0.1: Add Multi Company ability
  • +
  • 16.0.0.2: rename product creation methode
  • +
  • 16.0.0.3: Make validity_date persistent, add batches to SO views
  • +
  • 16.0.0.4: Remove validity date
  • +
  • 16.0.0.5: Move product logic to sale order lines
  • +
  • 16.0.2.1.0: Add “In Progress” and “Cancelled” state
  • +
  • 16.0.2.2.0: Add invoice status and menues
  • +
  • 16.0.2.2.1: Add lst_price to view
  • +
  • 16.0.2.3.0: Add “Reset to Open”
  • +
  • 16.0.2.3.1: Remove Sale Order from batch on cancelation
  • +
+
+
+

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
  • +
  • Niels Göttsch
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is part of the madmooose/odooapps 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..a245ba6 --- /dev/null +++ b/sale_order_batch/views/sale_menus.xml @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/sale_order_batch/views/sale_order_batch_product_views.xml b/sale_order_batch/views/sale_order_batch_product_views.xml new file mode 100644 index 0000000..2bf939a --- /dev/null +++ b/sale_order_batch/views/sale_order_batch_product_views.xml @@ -0,0 +1,52 @@ + + + + sale.order.batch.product.form + sale.order.batch.product + +
+
+ +
+ +
+
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
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..acb62b8 --- /dev/null +++ b/sale_order_batch/views/sale_order_batch_views.xml @@ -0,0 +1,338 @@ + + + + sale.order.batch.tree + sale.order.batch + + + + + + + + + + + + + + + sale.order.batch.quotation.tree + sale.order.batch + + + + + + + + + + + + + + sale.order.batch.form + sale.order.batch + +
+
+
+ + +
+ + + +
+

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+
+
+
+ + + sale.order.batch.view.search + sale.order.batch + + + + + + + + + + + Quotation Batches + ir.actions.act_window + sale.order.batch + tree,form,activity + + {'search_default_filter_batch_open': 1} + + + + Order Batches + ir.actions.act_window + sale.order.batch + tree,form,activity + + {'search_default_filter_batch_finished': 1} + + + + Batches to Invoice + ir.actions.act_window + sale.order.batch + tree,form,activity + {'create': False} + [('invoice_status','=','to invoice')] + + +

+ No batches to invoice found +

+
+
+
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..b2cfb34 --- /dev/null +++ b/sale_order_batch/views/sale_order_views.xml @@ -0,0 +1,85 @@ + + + + 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.tree.inherited.batch + sale.order + + + +
+
+
+ + + +
+
+ + + sale.order.tree.inherited.batch + sale.order + + + + + + + + +
diff --git a/sale_order_batch/wizard/__init__.py b/sale_order_batch/wizard/__init__.py new file mode 100644 index 0000000..56c3dec --- /dev/null +++ b/sale_order_batch/wizard/__init__.py @@ -0,0 +1 @@ +from . import sale_order_batch_cancel_wizard diff --git a/sale_order_batch/wizard/sale_order_batch_cancel_wizard.py b/sale_order_batch/wizard/sale_order_batch_cancel_wizard.py new file mode 100644 index 0000000..56333e3 --- /dev/null +++ b/sale_order_batch/wizard/sale_order_batch_cancel_wizard.py @@ -0,0 +1,34 @@ +from odoo import api, fields, models + + +class SaleOrderBatchCancelWizard(models.TransientModel): + _name = "sale.order.batch.cancel.wizard" + _description = "Cancel Sale Order Batch Confirmation" + + batch_ids = fields.Many2many( + comodel_name="sale.order.batch", + string="Batches to Cancel", + default=lambda self: self.env["sale.order.batch"].browse( + self.env.context.get("active_ids", []) + ), + ) + + order_ids = fields.Many2many( + comodel_name="sale.order", + string="Orders to Cancel", + compute="_compute_order_ids", + ) + + @api.depends("batch_ids") + def _compute_order_ids(self): + for record in self: + batches = record.batch_ids + record.order_ids = batches.mapped("sale_order_ids") + + def action_cancel(self): + batches = self.batch_ids + batches._action_cancel() + return {"type": "ir.actions.act_window_close"} + + def action_discard(self): + return {"type": "ir.actions.act_window_close"} diff --git a/sale_order_batch/wizard/sale_order_batch_cancel_wizard.xml b/sale_order_batch/wizard/sale_order_batch_cancel_wizard.xml new file mode 100644 index 0000000..0c221f9 --- /dev/null +++ b/sale_order_batch/wizard/sale_order_batch_cancel_wizard.xml @@ -0,0 +1,29 @@ + + + sale.order.batch.cancel.wizard.form + sale.order.batch.cancel.wizard + +
+ + + + + + + + + +
+
+
+