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 @@
+
diff --git a/addons/point_of_sale/static/src/xml/ChromeWidgets/CashMoveButton.xml b/addons/point_of_sale/static/src/xml/ChromeWidgets/CashMoveButton.xml new file mode 100644 index 00000000000..595599c111c --- /dev/null +++ b/addons/point_of_sale/static/src/xml/ChromeWidgets/CashMoveButton.xml @@ -0,0 +1,11 @@ + + + + +
+ + Cash In/Out +
+
+ +
diff --git a/addons/point_of_sale/static/src/xml/Popups/CashMovePopup.xml b/addons/point_of_sale/static/src/xml/Popups/CashMovePopup.xml new file mode 100644 index 00000000000..078461710d4 --- /dev/null +++ b/addons/point_of_sale/static/src/xml/Popups/CashMovePopup.xml @@ -0,0 +1,41 @@ + + + + + + + + diff --git a/addons/point_of_sale/static/src/xml/Popups/CashMoveReceipt.xml b/addons/point_of_sale/static/src/xml/Popups/CashMoveReceipt.xml new file mode 100644 index 00000000000..43db523074e --- /dev/null +++ b/addons/point_of_sale/static/src/xml/Popups/CashMoveReceipt.xml @@ -0,0 +1,70 @@ + + + + +
+ + +
+
+ +

+ +

+
+
+
+ +
+ +
+
+ +
Tel: + +
+
+ +
VAT: + +
+
+ +
+ +
+
+ +
+ +
+
+ +
+
--------------------------------
+
Done by + +
+
+
+
+
+ +
+ CASH + +
+
+
+ AMOUNT + +
+
+ REASON + +
+
+
+
+ +
diff --git a/addons/point_of_sale/views/pos_session_view.xml b/addons/point_of_sale/views/pos_session_view.xml index ab35e9d54c7..e12b467ded8 100644 --- a/addons/point_of_sale/views/pos_session_view.xml +++ b/addons/point_of_sale/views/pos_session_view.xml @@ -54,6 +54,15 @@ groups="account.group_account_readonly" > +

diff --git a/addons/pos_hr/static/src/js/Chrome.js b/addons/pos_hr/static/src/js/Chrome.js index eacc95610e5..3f2cc1ca4be 100644 --- a/addons/pos_hr/static/src/js/Chrome.js +++ b/addons/pos_hr/static/src/js/Chrome.js @@ -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);