[IMP] point_of_sale,pos_hr: allow cash in/out in the pos ui
Part-of: odoo/odoo#74870
This commit is contained in:
@@ -129,6 +129,7 @@
|
||||
'point_of_sale/static/src/js/ChromeWidgets/OrderManagementButton.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/HeaderButton.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/SaleDetailsButton.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js',
|
||||
'point_of_sale/static/src/js/ChromeWidgets/TicketButton.js',
|
||||
'point_of_sale/static/src/js/Misc/Draggable.js',
|
||||
'point_of_sale/static/src/js/Misc/NotificationSound.js',
|
||||
@@ -140,6 +141,7 @@
|
||||
'point_of_sale/static/src/js/Popups/ErrorPopup.js',
|
||||
'point_of_sale/static/src/js/Popups/ErrorBarcodePopup.js',
|
||||
'point_of_sale/static/src/js/Popups/ConfirmPopup.js',
|
||||
'point_of_sale/static/src/js/Popups/CashMovePopup.js',
|
||||
'point_of_sale/static/src/js/Popups/TextInputPopup.js',
|
||||
'point_of_sale/static/src/js/Popups/TextAreaPopup.js',
|
||||
'point_of_sale/static/src/js/Popups/ErrorTracebackPopup.js',
|
||||
|
||||
@@ -1159,6 +1159,15 @@ class PosSession(models.Model):
|
||||
# self should be single record as this method is only called in the subfunctions of self._validate_session
|
||||
return self.currency_id._convert(amount, self.company_id.currency_id, self.company_id, date, round=round)
|
||||
|
||||
def show_cash_register(self):
|
||||
return {
|
||||
'name': _('Cash register for %s') % (self.cash_register_id.name, ),
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.bank.statement',
|
||||
'view_mode': 'form',
|
||||
'res_id': self.cash_register_id.id,
|
||||
}
|
||||
|
||||
def show_journal_items(self):
|
||||
self.ensure_one()
|
||||
all_related_moves = self._get_related_account_moves()
|
||||
@@ -1290,6 +1299,14 @@ class PosSession(models.Model):
|
||||
)
|
||||
return True
|
||||
|
||||
def try_cash_in_out(self, _type, amount, reason, extras):
|
||||
sign = 1 if _type == 'in' else -1
|
||||
self.env['cash.box.out']\
|
||||
.with_context({'active_model': 'pos.session', 'active_ids': self.ids})\
|
||||
.create({'amount': sign * amount, 'name': reason})\
|
||||
.run()
|
||||
self.message_post(body='<br/>\n'.join([f"Cash {extras['translatedType']}", f'- Amount: {extras["formattedAmount"]}', f'- Reason: {reason}']))
|
||||
|
||||
class ProcurementGroup(models.Model):
|
||||
_inherit = 'procurement.group'
|
||||
|
||||
|
||||
@@ -3674,3 +3674,59 @@ td {
|
||||
.pos .pos-topheader .oe_status:hover {
|
||||
background: rgb(104,69,95);
|
||||
}
|
||||
|
||||
/** CashMovePopup **/
|
||||
|
||||
.cash-move {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
}
|
||||
|
||||
.cash-move .input-amount {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
}
|
||||
|
||||
.cash-move .input-amount .input-type {
|
||||
height: 40px;
|
||||
line-height:40px;
|
||||
text-align:center;
|
||||
border-radius: 2px;
|
||||
padding: 0 12px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border: solid 1px rgba(60,60,60,0.1);
|
||||
background: rgba(0,0,0,0.05);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cash-move .input-amount .input-type.highlight {
|
||||
background: #6EC89B !important;
|
||||
border: solid 1px #64AF8A !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.cash-move .input-amount input {
|
||||
width: 200px;
|
||||
min-height: auto;
|
||||
text-align: right;
|
||||
padding-right: 30px;
|
||||
}
|
||||
|
||||
.cash-move .input-amount .input-field span.currency {
|
||||
position: relative;
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
.cash-move textarea {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.cash-move .error-message {
|
||||
color: rgb(197, 52, 0);
|
||||
text-align: left;
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
@@ -449,6 +449,9 @@ odoo.define('point_of_sale.Chrome', function(require) {
|
||||
false
|
||||
);
|
||||
}
|
||||
showCashMoveButton() {
|
||||
return this.env.pos && this.env.pos.config && this.env.pos.config.cash_control;
|
||||
}
|
||||
}
|
||||
Chrome.template = 'Chrome';
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
odoo.define('point_of_sale.CashMoveButton', function (require) {
|
||||
'use strict';
|
||||
|
||||
const PosComponent = require('point_of_sale.PosComponent');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _t } = require('web.core');
|
||||
|
||||
const TRANSLATED_CASH_MOVE_TYPE = {
|
||||
in: _t('in'),
|
||||
out: _t('out'),
|
||||
};
|
||||
|
||||
class CashMoveButton extends PosComponent {
|
||||
async onClick() {
|
||||
const { confirmed, payload } = await this.showPopup('CashMovePopup');
|
||||
if (!confirmed) return;
|
||||
const { type, amount, reason } = payload;
|
||||
const translatedType = TRANSLATED_CASH_MOVE_TYPE[type];
|
||||
const formattedAmount = this.env.pos.format_currency(amount);
|
||||
if (!amount) {
|
||||
return this.showNotification(
|
||||
_.str.sprintf(this.env._t('Cash in/out of %s is ignored.'), formattedAmount),
|
||||
3000
|
||||
);
|
||||
}
|
||||
const extras = { formattedAmount, translatedType };
|
||||
await this.rpc({
|
||||
model: 'pos.session',
|
||||
method: 'try_cash_in_out',
|
||||
args: [[this.env.pos.pos_session.id], type, amount, reason, extras],
|
||||
});
|
||||
if (this.env.pos.proxy.printer) {
|
||||
const renderedReceipt = this.env.qweb.renderToString('point_of_sale.CashMoveReceipt', {
|
||||
_receipt: this._getReceiptInfo({ ...payload, translatedType, formattedAmount }),
|
||||
});
|
||||
const printResult = await this.env.pos.proxy.printer.print_receipt(renderedReceipt);
|
||||
if (!printResult.successful) {
|
||||
this.showPopup('ErrorPopup', { title: printResult.message.title, body: printResult.message.body });
|
||||
}
|
||||
}
|
||||
this.showNotification(
|
||||
_.str.sprintf(this.env._t('Successfully made a cash %s of %s.'), type, formattedAmount),
|
||||
3000
|
||||
);
|
||||
}
|
||||
_getReceiptInfo(payload) {
|
||||
const result = { ...payload };
|
||||
result.cashier = this.env.pos.get_cashier();
|
||||
result.company = this.env.pos.company;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
CashMoveButton.template = 'point_of_sale.CashMoveButton';
|
||||
|
||||
Registries.Component.add(CashMoveButton);
|
||||
|
||||
return CashMoveButton;
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
odoo.define('point_of_sale.CashMovePopup', function (require) {
|
||||
'use strict';
|
||||
|
||||
const AbstractAwaitablePopup = require('point_of_sale.AbstractAwaitablePopup');
|
||||
const Registries = require('point_of_sale.Registries');
|
||||
const { _t } = require('web.core');
|
||||
const { parse } = require('web.field_utils');
|
||||
|
||||
class CashMovePopup extends AbstractAwaitablePopup {
|
||||
setup() {
|
||||
this.state = owl.hooks.useState({
|
||||
inputType: '', // '' | 'in' | 'out'
|
||||
inputAmount: '',
|
||||
inputReason: '',
|
||||
inputHasError: false,
|
||||
});
|
||||
this.inputAmountRef = owl.hooks.useRef('input-amount-ref');
|
||||
}
|
||||
confirm() {
|
||||
try {
|
||||
parse.float(this.state.inputAmount);
|
||||
} catch (error) {
|
||||
this.state.inputHasError = true;
|
||||
this.errorMessage = this.env._t('Invalid amount');
|
||||
return;
|
||||
}
|
||||
if (this.state.inputType == '') {
|
||||
this.state.inputHasError = true;
|
||||
this.errorMessage = this.env._t('Select either Cash In or Cash Out before confirming.');
|
||||
return;
|
||||
}
|
||||
return super.confirm();
|
||||
}
|
||||
onClickButton(type) {
|
||||
this.state.inputType = type;
|
||||
this.state.inputHasError = false;
|
||||
this.inputAmountRef.el && this.inputAmountRef.el.focus();
|
||||
}
|
||||
getPayload() {
|
||||
return {
|
||||
amount: parse.float(this.state.inputAmount),
|
||||
reason: this.state.inputReason.trim(),
|
||||
type: this.state.inputType,
|
||||
};
|
||||
}
|
||||
}
|
||||
CashMovePopup.template = 'point_of_sale.CashMovePopup';
|
||||
CashMovePopup.defaultProps = {
|
||||
cancelText: _t('Cancel'),
|
||||
title: _t('Cash In/Out'),
|
||||
};
|
||||
|
||||
Registries.Component.add(CashMovePopup);
|
||||
|
||||
return CashMovePopup;
|
||||
});
|
||||
@@ -9,6 +9,7 @@
|
||||
<div class="pos-branding" t-if= "!env.isMobile">
|
||||
<img class="pos-logo" t-on-click="trigger('toggle-debug-widget')"
|
||||
src="/point_of_sale/static/src/img/logo.png" alt="Logo" />
|
||||
<CashMoveButton t-if="showCashMoveButton()" />
|
||||
<TicketButton isTicketScreenShown="isTicketScreenShown" />
|
||||
</div>
|
||||
<div class="pos-rightheader">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.CashMoveButton" owl="1">
|
||||
<div class="ticket-button" t-on-click="onClick">
|
||||
<i class="fa fa-money" aria-hidden="true"></i>
|
||||
<span style="padding-left: 10px;">Cash In/Out</span>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.CashMovePopup" owl="1">
|
||||
<div role="dialog" class="modal-dialog">
|
||||
<Draggable>
|
||||
<div class="popup drag-handle">
|
||||
<main class="body">
|
||||
<div class="cash-move">
|
||||
<div class="input-amount">
|
||||
<span t-on-click="onClickButton('in')" class="input-type" t-att-class="{ highlight: state.inputType == 'in' }">
|
||||
Cash In
|
||||
</span>
|
||||
<span t-on-click="onClickButton('out')" class="input-type" t-att-class="{ highlight: state.inputType == 'out' }">
|
||||
Cash Out
|
||||
</span>
|
||||
<div class="input-field">
|
||||
<input type="text" name="amount" t-model="state.inputAmount" t-ref="input-amount-ref"/>
|
||||
<span class="currency" t-esc="env.pos.currency.symbol" />
|
||||
</div>
|
||||
</div>
|
||||
<textarea name="reason" t-model="state.inputReason" placeholder="Reason"></textarea>
|
||||
<span t-if="state.inputHasError" class="error-message">
|
||||
<t t-esc="errorMessage" />
|
||||
</span>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="footer">
|
||||
<div class="button confirm disable" t-on-click="confirm">
|
||||
Confirm
|
||||
</div>
|
||||
<div class="button cancel" t-on-click="cancel">
|
||||
<t t-esc="props.cancelText" />
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</Draggable>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
|
||||
<t t-name="point_of_sale.CashMoveReceipt" owl="1">
|
||||
<div class="pos-receipt">
|
||||
<t t-if="_receipt.company.logo">
|
||||
<img class="pos-receipt-logo" t-att-src="_receipt.company.logo" alt="Logo" />
|
||||
<br />
|
||||
</t>
|
||||
<t t-if="!_receipt.company.logo">
|
||||
<h2 class="pos-receipt-center-align">
|
||||
<t t-esc="_receipt.company.name" />
|
||||
</h2>
|
||||
<br />
|
||||
</t>
|
||||
<div class="pos-receipt-contact">
|
||||
<t t-if="_receipt.company.contact_address">
|
||||
<div>
|
||||
<t t-esc="_receipt.company.contact_address" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="_receipt.company.phone">
|
||||
<div>Tel:
|
||||
<t t-esc="_receipt.company.phone" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="_receipt.company.vat">
|
||||
<div>VAT:
|
||||
<t t-esc="_receipt.company.vat" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="_receipt.company.email">
|
||||
<div>
|
||||
<t t-esc="_receipt.company.email" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="_receipt.company.website">
|
||||
<div>
|
||||
<t t-esc="_receipt.company.website" />
|
||||
</div>
|
||||
</t>
|
||||
<t t-if="_receipt.cashier">
|
||||
<div class="cashier">
|
||||
<div>--------------------------------</div>
|
||||
<div>Done by
|
||||
<t t-esc="_receipt.cashier.name" />
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
<div class="pos-receipt-center-align">
|
||||
CASH
|
||||
<t t-esc="_receipt.translatedType.toUpperCase()" />
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
AMOUNT
|
||||
<span t-esc="_receipt.formattedAmount" class="pos-receipt-right-align" />
|
||||
</div>
|
||||
<div>
|
||||
REASON
|
||||
<span t-esc="_receipt.reason" class="pos-receipt-right-align" />
|
||||
</div>
|
||||
<br />
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
@@ -54,6 +54,15 @@
|
||||
groups="account.group_account_readonly"
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
name="show_cash_register"
|
||||
type="object"
|
||||
class="oe_stat_button"
|
||||
icon="fa-bars"
|
||||
string="Cash Register"
|
||||
attrs="{'invisible':[('cash_control', '=', False)]}"
|
||||
groups="account.group_account_readonly"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="oe_title">
|
||||
<field name="name" attrs="{'invisible': [('name','=','/')]}" class="oe_inline"/>
|
||||
|
||||
@@ -14,6 +14,9 @@ odoo.define('pos_hr.chrome', function (require) {
|
||||
get headerButtonIsShown() {
|
||||
return !this.env.pos.config.module_pos_hr || this.env.pos.get('cashier').role == 'manager';
|
||||
}
|
||||
showCashMoveButton() {
|
||||
return super.showCashMoveButton() && this.env.pos.get('cashier').role == 'manager';
|
||||
}
|
||||
};
|
||||
|
||||
Registries.Component.extend(Chrome, PosHrChrome);
|
||||
|
||||
Reference in New Issue
Block a user