diff --git a/addons/point_of_sale/__manifest__.py b/addons/point_of_sale/__manifest__.py
index ec1659b0041..5751921a465 100644
--- a/addons/point_of_sale/__manifest__.py
+++ b/addons/point_of_sale/__manifest__.py
@@ -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',
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 3c7f15c90b6..04e09fa5353 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -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='
\n'.join([f"Cash {extras['translatedType']}", f'- Amount: {extras["formattedAmount"]}', f'- Reason: {reason}']))
+
class ProcurementGroup(models.Model):
_inherit = 'procurement.group'
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index 7d17250dff5..4e08726790e 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -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;
+}
diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js
index 246dcaa4f08..b65f6b50eda 100644
--- a/addons/point_of_sale/static/src/js/Chrome.js
+++ b/addons/point_of_sale/static/src/js/Chrome.js
@@ -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';
diff --git a/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js b/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js
new file mode 100644
index 00000000000..e35a5200486
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/ChromeWidgets/CashMoveButton.js
@@ -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;
+});
diff --git a/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js b/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js
new file mode 100644
index 00000000000..7c46d92b2e2
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/Popups/CashMovePopup.js
@@ -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;
+});
diff --git a/addons/point_of_sale/static/src/xml/Chrome.xml b/addons/point_of_sale/static/src/xml/Chrome.xml
index 910e3c76974..10c9c3e074c 100644
--- a/addons/point_of_sale/static/src/xml/Chrome.xml
+++ b/addons/point_of_sale/static/src/xml/Chrome.xml
@@ -9,6 +9,7 @@
+