Files
odoo-addons/addons/laundry_management/wizard/laundry_session_wizard.py

231 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}'
),
)