from odoo import models, fields, api class LaundryDashboard(models.TransientModel): """Live KPI dashboard — queries sale.order with is_laundry_order = True.""" _name = 'laundry.dashboard' _description = 'Laundry Dashboard' today_orders = fields.Integer(string="Today's Orders") today_revenue = fields.Monetary(string="Today's Revenue", currency_field='currency_id') today_collected = fields.Monetary(string='Collected Today', currency_field='currency_id') today_outstanding = fields.Monetary(string='Outstanding Today', currency_field='currency_id') pending_count = fields.Integer(string='Pending Orders') ready_count = fields.Integer(string='Ready for Pickup') in_progress_count = fields.Integer(string='In Processing') draft_count = fields.Integer(string='Quotes / Draft') session_is_open = fields.Boolean(string='Session Open') session_name = fields.Char(string='Session') session_opening_cash = fields.Monetary(string='Opening Float', currency_field='currency_id') session_sales = fields.Monetary(string='Session Sales', currency_field='currency_id') session_cash = fields.Monetary(string='Session Cash', currency_field='currency_id') session_bank = fields.Monetary(string='Session Bank', currency_field='currency_id') session_id = fields.Many2one('laundry.session', string='Session Link') month_orders = fields.Integer(string='Orders This Month') month_revenue = fields.Monetary(string='Revenue This Month', currency_field='currency_id') month_paid = fields.Monetary(string='Collected This Month', currency_field='currency_id') currency_id = fields.Many2one( 'res.currency', default=lambda self: self.env.company.currency_id, ) @api.model def _build(self): today = fields.Date.today() month_start = today.replace(day=1) company = self.env.company Order = self.env['sale.order'] Payment = self.env['account.payment'] _base_domain = [ ('is_laundry_order', '=', True), ('company_id', '=', company.id), ] # ── Today ────────────────────────────────────────────────────── today_orders = Order.search(_base_domain + [ ('date_order', '>=', fields.Datetime.to_datetime(today)), ('state', 'not in', ['cancel', 'draft']), ]) today_invoices = today_orders.mapped('invoice_ids').filtered( lambda i: i.state == 'posted' and i.move_type == 'out_invoice' ) today_revenue = sum(today_orders.mapped('amount_total')) today_outstanding = sum( max(i.amount_residual, 0.0) for i in today_invoices ) today_collected = today_revenue - today_outstanding # ── Pipeline (all active laundry orders) ────────────────────── pipeline = Order.search(_base_domain + [ ('state', '=', 'sale'), ]) pending_count = len(pipeline) ready_count = len(pipeline.filtered(lambda o: o.laundry_state == 'ready')) in_progress_count = len(pipeline.filtered(lambda o: o.laundry_state == 'processing')) draft_count = len(Order.search(_base_domain + [('state', '=', 'draft')])) # ── Session ──────────────────────────────────────────────────── session = self.env['laundry.session'].search([ ('state', '=', 'opened'), ('company_id', '=', company.id), ], limit=1) # ── Month ────────────────────────────────────────────────────── month_orders = Order.search(_base_domain + [ ('date_order', '>=', fields.Datetime.to_datetime(month_start)), ('state', 'not in', ['cancel', 'draft']), ]) month_invoices = month_orders.mapped('invoice_ids').filtered( lambda i: i.state == 'posted' and i.move_type == 'out_invoice' ) month_revenue = sum(month_orders.mapped('amount_total')) month_outstanding = sum(max(i.amount_residual, 0.0) for i in month_invoices) month_paid = month_revenue - month_outstanding return self.create({ 'today_orders' : len(today_orders), 'today_revenue' : today_revenue, 'today_collected' : max(today_collected, 0.0), 'today_outstanding' : today_outstanding, 'pending_count' : pending_count, 'ready_count' : ready_count, 'in_progress_count' : in_progress_count, 'draft_count' : draft_count, 'session_is_open' : bool(session), 'session_name' : session.name if session else '', 'session_opening_cash' : session.opening_cash if session else 0.0, 'session_sales' : session.total_sales if session else 0.0, 'session_cash' : session.total_cash if session else 0.0, 'session_bank' : session.total_bank if session else 0.0, 'session_id' : session.id if session else False, 'month_orders' : len(month_orders), 'month_revenue' : month_revenue, 'month_paid' : max(month_paid, 0.0), }) @api.model def action_open_dashboard(self): rec = self._build() return { 'type': 'ir.actions.act_window', 'name': 'Dashboard', 'res_model': 'laundry.dashboard', 'res_id': rec.id, 'view_mode': 'form', 'target': 'main', 'flags': {'mode': 'readonly'}, } def action_refresh(self): return self.action_open_dashboard() def action_new_order(self): return { 'type': 'ir.actions.act_window', 'name': 'New Laundry Order', 'res_model': 'sale.order', 'view_mode': 'form', 'target': 'current', 'context': { 'default_is_laundry_order': True, }, } def action_open_session(self): if self.session_id: return { 'type': 'ir.actions.act_window', 'name': 'Session', 'res_model': 'laundry.session', 'res_id': self.session_id.id, 'view_mode': 'form', } return { 'type': 'ir.actions.act_window', 'name': 'Sessions', 'res_model': 'laundry.session', 'view_mode': 'list,form', } def action_new_session(self): return { 'type': 'ir.actions.act_window', 'name': 'New Session', 'res_model': 'laundry.session', 'view_mode': 'form', } def action_view_ready_orders(self): return { 'type': 'ir.actions.act_window', 'name': 'Ready for Pickup', 'res_model': 'sale.order', 'view_mode': 'list,form', 'domain': [('is_laundry_order', '=', True), ('laundry_state', '=', 'ready')], } def action_view_pending_orders(self): return { 'type': 'ir.actions.act_window', 'name': 'Pending Orders', 'res_model': 'sale.order', 'view_mode': 'list,form', 'domain': [('is_laundry_order', '=', True), ('state', '=', 'sale'), ('laundry_state', 'not in', ['delivered'])], }