[IMP] point_of_sale,pos_hr: allow cash in/out in the pos ui

Part-of: odoo/odoo#74870
This commit is contained in:
Joseph Caburnay
2021-08-27 17:43:09 +00:00
parent c0f879f6b8
commit 659aa50522
12 changed files with 327 additions and 0 deletions
+2
View File
@@ -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"/>
+3
View File
@@ -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);