feat: add cash control details including expected, counted, and difference amounts to POS closing receipts and reports

This commit is contained in:
Suherdy Yacob 2026-06-09 09:38:07 +07:00
parent e319ecdb72
commit 733d52e2d7
6 changed files with 101 additions and 1 deletions

View File

@ -43,6 +43,7 @@ TOTAL Rp 850.000,00
|---------------|----------------------------------------|
| `point_of_sale` | Core Odoo 19 POS module |
| `pos_hr` | Required for cashier name lookup via `pos.getCashier()` |
| `pos_cash_opening_adjustment` | Required for cash difference tracking fields (`closing_cash_expected`, `closing_cash_counted`, etc.) |
---

View File

@ -15,7 +15,7 @@
form view in the Odoo backend for easy reprinting.
""",
'author': 'Suherdy Yacob',
'depends': ['point_of_sale', 'pos_hr'],
'depends': ['point_of_sale', 'pos_hr', 'pos_cash_opening_adjustment'],
'data': [
'report/pos_closing_summary_report.xml',
'views/pos_session_views.xml',

View File

@ -99,6 +99,10 @@ class PosSession(models.Model):
'cash_payment': cash_payment,
'non_cash_payments': non_cash_payments,
'grand_total': grand_total,
'has_cash_control': session.config_id.cash_control,
'closing_cash_expected': session.closing_cash_expected,
'closing_cash_counted': session.closing_cash_counted,
'closing_cash_difference': session.closing_cash_difference,
}
@api.model
@ -162,4 +166,8 @@ class PosSession(models.Model):
'cashier_name': cashier_name,
'closing_time': closing_time,
'payment_methods': payment_methods,
'has_cash_control': session.config_id.cash_control,
'closing_cash_expected': session.closing_cash_expected,
'closing_cash_counted': session.closing_cash_counted,
'closing_cash_difference': session.closing_cash_difference,
}

View File

@ -127,6 +127,34 @@
</tr>
</table>
<!-- ===== CASH CONTROL DETAILS ===== -->
<t t-if="data.get('has_cash_control')">
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 8px;">
<tr>
<td style="padding: 5px 0;">Expected Cash</td>
<td style="text-align: right; padding: 5px 0;">
<t t-esc="data['closing_cash_expected']"
t-options="{'widget': 'monetary', 'display_currency': session.currency_id}"/>
</td>
</tr>
<tr>
<td style="padding: 5px 0;">Counted Cash</td>
<td style="text-align: right; padding: 5px 0;">
<t t-esc="data['closing_cash_counted']"
t-options="{'widget': 'monetary', 'display_currency': session.currency_id}"/>
</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 5px 0;">Cash Difference</td>
<td style="text-align: right; font-weight: bold; padding: 5px 0;">
<t t-esc="data['closing_cash_difference']"
t-options="{'widget': 'monetary', 'display_currency': session.currency_id}"/>
</td>
</tr>
</table>
</t>
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<!-- ===== FOOTER ===== -->

View File

@ -79,6 +79,31 @@
</tr>
</table>
<!-- ===== CASH CONTROL DETAILS ===== -->
<t t-if="props.hasCashControl">
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 8px;">
<tr>
<td style="padding: 5px 0;">Expected Cash</td>
<td style="text-align: right; padding: 5px 0;">
<t t-esc="props.expectedCash"/>
</td>
</tr>
<tr>
<td style="padding: 5px 0;">Counted Cash</td>
<td style="text-align: right; padding: 5px 0;">
<t t-esc="props.countedCash"/>
</td>
</tr>
<tr>
<td style="font-weight: bold; padding: 5px 0;">Cash Difference</td>
<td style="text-align: right; font-weight: bold; padding: 5px 0;">
<t t-esc="props.cashDifference"/>
</td>
</tr>
</table>
</t>
<div style="border-top: 1px dashed #000; margin: 8px 0;"/>
<!-- ===== FOOTER ===== -->

View File

@ -348,6 +348,18 @@ patch(ClosePosPopup.prototype, {
const nonCashTotal = nonCashPayments.reduce((sum, pm) => sum + pm.amount, 0);
const grandTotal = formatCurrency(cashAmount + nonCashTotal);
let expectedCash = 0;
let countedCash = 0;
let cashDifference = 0;
let hasCashControl = false;
if (this.props.default_cash_details) {
hasCashControl = true;
expectedCash = this.props.default_cash_details.amount || 0;
const countedStr = this.state.payments[this.props.default_cash_details.id]?.counted || "0";
countedCash = parseFloat(countedStr) || 0;
cashDifference = countedCash - expectedCash;
}
return {
sessionName,
cashierName,
@ -355,6 +367,11 @@ patch(ClosePosPopup.prototype, {
cashPayment,
nonCashPayments,
grandTotal,
hasCashControl,
expectedCash: formatCurrency(expectedCash),
countedCash: formatCurrency(countedCash),
cashDifference: formatCurrency(cashDifference),
rawCashDifference: cashDifference,
};
},
});
@ -414,6 +431,26 @@ patch(Navbar.prototype, {
.reduce((s, p) => s + p.amount, 0);
const grandTotal = formatCurrency(cashAmount + nonCashTotal);
let cashControlBlock = "";
if (sessionData.has_cash_control) {
cashControlBlock = `
<div class="dashed"></div>
<table>
<tr>
<td style="padding:5px 0;">Expected Cash</td>
<td class="right" style="padding:5px 0;">${formatCurrency(sessionData.closing_cash_expected)}</td>
</tr>
<tr>
<td style="padding:5px 0;">Counted Cash</td>
<td class="right" style="padding:5px 0;">${formatCurrency(sessionData.closing_cash_counted)}</td>
</tr>
<tr>
<td class="bold" style="padding:5px 0;">Cash Difference</td>
<td class="right bold" style="padding:5px 0;">${formatCurrency(sessionData.closing_cash_difference)}</td>
</tr>
</table>`;
}
// ── 3. Build standalone HTML receipt and open print window ───────────
const html = `<!DOCTYPE html>
<html>
@ -500,6 +537,7 @@ patch(Navbar.prototype, {
<td class="right">${grandTotal}</td>
</tr>
</table>
${cashControlBlock}
<div class="dashed"></div>
<div class="footer">*** Session Closed ***</div>
<div class="feed">&nbsp;</div>