Tower: upload laundry_management 19.0.19.0.4 (via marketplace)
This commit is contained in:
230
addons/laundry_management/wizard/laundry_session_wizard.py
Normal file
230
addons/laundry_management/wizard/laundry_session_wizard.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
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}'
|
||||||
|
),
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user