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"}