from odoo import models, fields, api from odoo.exceptions import UserError class LaundrySessionCloseWizard(models.TransientModel): """Session closing wizard. Summarises the session's accounting figures (from account.payment / invoices) and asks the operator to count and enter the actual physical cash. On confirm: 1. If cash_difference != 0 and post_difference_entry is True: → Posts a journal entry: Cash ↔ Difference Account 2. Marks the session closed, records actual cash count. """ _name = 'laundry.session.close.wizard' _description = 'Close Laundry Session' session_id = fields.Many2one( 'laundry.session', string='Session', required=True, readonly=True, ) # ── Session summary (read-only, from session computed fields) ───── opening_cash = fields.Float( related='session_id.opening_cash', string='Opening Float', readonly=True, digits=(10, 2), ) total_sales = fields.Float( related='session_id.total_sales', string='Total Sales', readonly=True, digits=(10, 2), ) total_cash = fields.Float( related='session_id.total_cash', string='Cash Received', readonly=True, digits=(10, 2), ) total_bank = fields.Float( related='session_id.total_bank', string='Bank / Card', readonly=True, digits=(10, 2), ) total_credit = fields.Float( related='session_id.total_credit', string='Outstanding / Deferred', readonly=True, digits=(10, 2), ) total_paid = fields.Float( related='session_id.total_paid', string='Total Collected', readonly=True, digits=(10, 2), ) outstanding_amount = fields.Float( related='session_id.outstanding_amount', string='Outstanding', readonly=True, digits=(10, 2), ) expected_closing_cash = fields.Float( related='session_id.expected_closing_cash', string='Expected Cash in Drawer', readonly=True, digits=(10, 2), ) order_count = fields.Integer( related='session_id.order_count', string='Orders Processed', readonly=True, ) # ── Operator input ──────────────────────────────────────────────── actual_closing_cash = fields.Float( string='Actual Cash Count', digits=(10, 2), help='Count the physical cash in the drawer and enter the total here.', ) cash_difference = fields.Float( string='Difference (Actual − Expected)', compute='_compute_difference', digits=(10, 2), ) post_difference_entry = fields.Boolean( string='Post Difference Journal Entry', default=True, ) difference_account_id = fields.Many2one( 'account.account', string='Difference Account', help='Account to post cash count variance against.', ) closing_notes = fields.Text(string='Closing Notes') # ── Computed ────────────────────────────────────────────────────── @api.depends('actual_closing_cash', 'expected_closing_cash') def _compute_difference(self): for wiz in self: wiz.cash_difference = ( wiz.actual_closing_cash - wiz.expected_closing_cash ) @api.onchange('post_difference_entry') def _onchange_post_difference(self): if self.post_difference_entry and not self.difference_account_id: ICP = self.env['ir.config_parameter'].sudo() # 1. Try configured difference account from Laundry Settings configured_id = ICP.get_param('laundry_management.difference_account_id', '') if configured_id: try: acc = self.env['account.account'].browse(int(configured_id)) if acc.exists(): self.difference_account_id = acc return except (ValueError, TypeError): pass # 2. Fallback: first cash-difference account on the company company = ( self.session_id.company_id if self.session_id else self.env.company ) acc = ( company.default_cash_difference_expense_account_id or company.default_cash_difference_income_account_id ) if acc: self.difference_account_id = acc # ── Confirm ─────────────────────────────────────────────────────── def action_close(self): self.ensure_one() session = self.session_id if session.state != 'opened': raise UserError('Session is not open — cannot close.') diff = self.actual_closing_cash - self.expected_closing_cash if self.post_difference_entry and abs(diff) > 0.001: if not self.difference_account_id: raise UserError( 'Select a Difference Account before closing, ' 'or uncheck "Post Difference Journal Entry".' ) self._post_cash_difference_entry(session, diff) session.write({ 'state': 'closed', 'closing_datetime': fields.Datetime.now(), 'actual_closing_cash': self.actual_closing_cash, 'notes': self.closing_notes or session.notes or '', }) session.message_post( body=( f'Session closed by {self.env.user.name}. ' f'Orders: {self.order_count} | ' f'Sales: {self.total_sales:.2f} | ' f'Cash: {self.total_cash:.2f} | ' f'Bank: {self.total_bank:.2f} | ' f'Actual: {self.actual_closing_cash:.2f} | ' f'Diff: {diff:+.2f}' ), ) return {'type': 'ir.actions.act_window_close'} def _post_cash_difference_entry(self, session, difference): """Post a journal entry for the cash count variance. Surplus (difference > 0): Dr Cash / Cr Variance Account Shortage (difference < 0): Dr Variance Account / Cr Cash Journal priority: 1. Laundry default cash journal from Settings 2. First cash journal for the company """ company = session.company_id or self.env.company ICP = self.env['ir.config_parameter'].sudo() # 1. Try configured default from Laundry Settings configured_journal_id = ICP.get_param('laundry_management.cash_journal_id', '') cash_journal = False if configured_journal_id: try: cash_journal = self.env['account.journal'].browse(int(configured_journal_id)) if not cash_journal.exists() or cash_journal.type != 'cash': cash_journal = False except (ValueError, TypeError): cash_journal = False # 2. Fallback: first cash journal for the company if not cash_journal: cash_journal = self.env['account.journal'].search([ ('type', '=', 'cash'), ('company_id', '=', company.id), ], limit=1) if not cash_journal: raise UserError( 'No Cash journal found. ' 'Configure one in Laundry → Configuration → Settings → Default Cash Journal, ' 'or in Accounting → Journals.' ) cash_account = cash_journal.default_account_id if not cash_account: raise UserError( f'Cash journal "{cash_journal.name}" has no default account.' ) amount = abs(difference) if difference > 0: debit_account, credit_account = cash_account, self.difference_account_id else: debit_account, credit_account = self.difference_account_id, cash_account move = self.env['account.move'].create({ 'move_type' : 'entry', 'journal_id': cash_journal.id, 'date' : fields.Date.today(), 'ref' : f'Cash variance — {session.name}', 'company_id': company.id, 'line_ids': [ (0, 0, { 'account_id': debit_account.id, 'name' : f'Cash difference — {session.name}', 'debit' : amount, 'credit' : 0.0, }), (0, 0, { 'account_id': credit_account.id, 'name' : f'Cash difference — {session.name}', 'debit' : 0.0, 'credit' : amount, }), ], }) move.action_post() session.message_post( body=( f'Cash variance entry posted: ' f'{company.currency_id.symbol}{amount:.2f} ' f'({"surplus" if difference > 0 else "shortage"}). ' f'Entry: {move.name}' ), )