diff --git a/addons/gift_card/__manifest__.py b/addons/gift_card/__manifest__.py index 67c583400e9..66150174ae0 100644 --- a/addons/gift_card/__manifest__.py +++ b/addons/gift_card/__manifest__.py @@ -3,16 +3,14 @@ { 'name': "Gift Card", - 'summary': "Use gift card in your sales orders", - 'description': """Integrate gift card mechanism in sales orders.""", + 'summary': "Use gift card", + 'description': """Integrate gift card mechanism""", 'category': 'Sales/Sales', 'version': '1.0', - 'depends': ['sale'], + 'depends': ['product'], 'data': [ - 'data/mail_template_data.xml', 'data/gift_card_data.xml', 'views/views.xml', - 'views/templates.xml', 'security/ir.model.access.csv', ], 'license': 'LGPL-3', diff --git a/addons/gift_card/data/gift_card_data.xml b/addons/gift_card/data/gift_card_data.xml index 3fb702da8b6..4697d4e7f17 100644 --- a/addons/gift_card/data/gift_card_data.xml +++ b/addons/gift_card/data/gift_card_data.xml @@ -8,8 +8,6 @@ service - - @@ -20,7 +18,6 @@ service - diff --git a/addons/gift_card/models/__init__.py b/addons/gift_card/models/__init__.py index b65c1780b11..798243253f1 100644 --- a/addons/gift_card/models/__init__.py +++ b/addons/gift_card/models/__init__.py @@ -3,4 +3,3 @@ from . import gift_card from . import product -from . import sale_order diff --git a/addons/gift_card/models/gift_card.py b/addons/gift_card/models/gift_card.py index 8acc1763a51..516a392c7ed 100644 --- a/addons/gift_card/models/gift_card.py +++ b/addons/gift_card/models/gift_card.py @@ -13,10 +13,10 @@ class GiftCard(models.Model): @api.model def _generate_code(self): - return str(uuid4())[4:-8] + return '044' + str(uuid4())[4:-8][3:] name = fields.Char(compute='_compute_name') - code = fields.Char(default=_generate_code, required=True, readonly=True, copy=False) + code = fields.Char(default=lambda x: x._generate_code(), required=True, readonly=True, copy=False) partner_id = fields.Many2one('res.partner', help="If empty, all users can use it") company_id = fields.Many2one('res.company', default=lambda self: self.env.company) currency_id = fields.Many2one('res.currency', readonly=True, related='company_id.currency_id') @@ -29,10 +29,6 @@ class GiftCard(models.Model): copy=False ) - buy_line_id = fields.Many2one("sale.order.line", copy=False, readonly=True, - help="Sale Order line where this gift card has been bought.") - redeem_line_ids = fields.One2many('sale.order.line', 'gift_card_id', string="Redeems") - _sql_constraints = [ ('unique_gift_card_code', 'UNIQUE(code)', 'The gift card code must be unique.'), ('check_amount', 'CHECK(initial_amount >= 0)', 'The initial amount must be positive.') @@ -42,17 +38,6 @@ class GiftCard(models.Model): for record in self: record.name = _("Gift #%s", record.id) - @api.depends("initial_amount", "redeem_line_ids") - def _compute_balance(self): - for record in self: - balance = record.initial_amount - confirmed_line = record.redeem_line_ids.filtered(lambda l: l.state == 'sale') - if confirmed_line: - balance -= sum(confirmed_line.mapped( - lambda line: line.currency_id._convert(line.price_unit, record.currency_id, record.env.company, line.create_date) * -1 - )) - record.balance = balance - @api.autovacuum def _gc_mark_expired_gift_card(self): self.env['gift.card'].search([ @@ -66,3 +51,8 @@ class GiftCard(models.Model): def can_be_used(self): # expired state are computed once a day, so can be not synchro return self.state == 'valid' and self.balance > 0 and self.expired_date >= fields.Date.today() + + @api.depends("initial_amount") + def _compute_balance(self): + for record in self: + record.balance = record.initial_amount diff --git a/addons/gift_card/security/ir.model.access.csv b/addons/gift_card/security/ir.model.access.csv index 8fbe0cc2d13..64ddf646f26 100644 --- a/addons/gift_card/security/ir.model.access.csv +++ b/addons/gift_card/security/ir.model.access.csv @@ -1,4 +1,2 @@ id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink access_gift_card_all,Gift Card Program All,model_gift_card,,0,0,0,0 -access_gift_card_sales,Gift Card Program Salesman,model_gift_card,sales_team.group_sale_salesman,1,0,0,0 -access_gift_card_manager,Gift Card Program Sale Manager,model_gift_card,sales_team.group_sale_manager,1,1,1,1 diff --git a/addons/gift_card/views/views.xml b/addons/gift_card/views/views.xml index 28ed6652848..19169d9f60b 100644 --- a/addons/gift_card/views/views.xml +++ b/addons/gift_card/views/views.xml @@ -1,13 +1,6 @@ - - Gift Cards - gift.card - tree,form - [('buy_line_id.order_id', '=', active_id)] - - Gift Cards gift.card @@ -39,25 +32,17 @@ - - + - - - - - - - @@ -74,21 +59,4 @@ - - - sale.order.view.form.inherit.gift.card - sale.order - - - - - - - diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py index d3198afa72e..db3de94b718 100644 --- a/addons/point_of_sale/models/pos_order.py +++ b/addons/point_of_sale/models/pos_order.py @@ -741,13 +741,7 @@ class PosOrder(models.Model): 'target': 'current', } - def action_receipt_to_customer(self, name, client, ticket): - if not self: - return False - if not client.get('email'): - return False - - message = _("

Dear %s,
Here is your electronic ticket for the %s.

") % (client['name'], name) + def _add_mail_attachment(self, name, ticket): filename = 'Receipt-' + name + '.jpg' receipt = self.env['ir.attachment'].create({ 'name': filename, @@ -758,19 +752,12 @@ class PosOrder(models.Model): 'store_fname': filename, 'mimetype': 'image/jpeg', }) - mail_values = { - 'subject': _('Receipt %s', name), - 'body_html': message, - 'author_id': self.env.user.partner_id.id, - 'email_from': self.env.company.email or self.env.user.email_formatted, - 'email_to': client['email'], - 'attachment_ids': [(4, receipt.id)], - } + attachment = [(4, receipt.id)] if self.mapped('account_move'): report = self.env.ref('account.account_invoices')._render_qweb_pdf(self.account_move.ids[0]) filename = name + '.pdf' - attachment = self.env['ir.attachment'].create({ + invoice = self.env['ir.attachment'].create({ 'name': filename, 'type': 'binary', 'datas': base64.b64encode(report[0]), @@ -779,7 +766,26 @@ class PosOrder(models.Model): 'res_id': self.ids[0], 'mimetype': 'application/x-pdf' }) - mail_values['attachment_ids'] += [(4, attachment.id)] + attachment += [(4, invoice.id)] + + return attachment + + def action_receipt_to_customer(self, name, client, ticket): + if not self: + return False + if not client.get('email'): + return False + + message = _("

Dear %s,
Here is your electronic ticket for the %s.

") % (client['name'], name) + + mail_values = { + 'subject': _('Receipt %s', name), + 'body_html': message, + 'author_id': self.env.user.partner_id.id, + 'email_from': self.env.company.email or self.env.user.email_formatted, + 'email_to': client['email'], + 'attachment_ids': self._add_mail_attachment(name, ticket), + } mail = self.env['mail.mail'].sudo().create(mail_values) mail.send() diff --git a/addons/point_of_sale/models/res_config_settings.py b/addons/point_of_sale/models/res_config_settings.py index c3cfc256d99..0927302a09d 100644 --- a/addons/point_of_sale/models/res_config_settings.py +++ b/addons/point_of_sale/models/res_config_settings.py @@ -13,6 +13,7 @@ class ResConfigSettings(models.TransientModel): update_stock_quantities = fields.Selection(related="company_id.point_of_sale_update_stock_quantities", readonly=False) module_pos_coupon = fields.Boolean("Coupon and Promotion Programs", help="Allow the use of coupon and promotion programs in PoS.") account_default_pos_receivable_account_id = fields.Many2one(string='Default Account Receivable (PoS)', related='company_id.account_default_pos_receivable_account_id', readonly=False) + module_pos_gift_card = fields.Boolean("Gift Cards", help="Allow the use of gift card") def set_values(self): super(ResConfigSettings, self).set_values() diff --git a/addons/point_of_sale/views/res_config_settings_views.xml b/addons/point_of_sale/views/res_config_settings_views.xml index 77242118dc4..4884bfb185f 100644 --- a/addons/point_of_sale/views/res_config_settings_views.xml +++ b/addons/point_of_sale/views/res_config_settings_views.xml @@ -77,6 +77,17 @@ +
+
+ +
+
+
+

Payments

diff --git a/addons/pos_gift_card/__init__.py b/addons/pos_gift_card/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/pos_gift_card/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/pos_gift_card/__manifest__.py b/addons/pos_gift_card/__manifest__.py new file mode 100644 index 00000000000..d248853a47c --- /dev/null +++ b/addons/pos_gift_card/__manifest__.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Gift Card for point of sales module", + 'summary': "Use gift card in your sales orders", + 'description': """Integrate gift card mechanism in sales orders.""", + 'category': 'Sales/Sales', + 'version': '1.0', + 'depends': ['gift_card', 'point_of_sale'], + 'auto_install': True, + 'data': [ + 'data/gift_card_data.xml', + 'views/gift_card_views.xml', + 'views/res_config_settings_views.xml', + 'views/pos_config_views.xml', + 'security/ir.model.access.csv', + ], + 'assets': { + 'point_of_sale.assets': [ + 'pos_gift_card/static/src/css/giftCard.css', + 'pos_gift_card/static/src/js/models.js', + 'pos_gift_card/static/src/js/GiftCardButton.js', + 'pos_gift_card/static/src/js/GiftCardPopup.js', + 'pos_gift_card/static/src/js/PaymentScreen.js', + ], + 'web.assets_qweb': [ + 'pos_gift_card/static/src/xml/**/*', + ], + }, + 'license': 'LGPL-3', +} diff --git a/addons/pos_gift_card/data/gift_card_data.xml b/addons/pos_gift_card/data/gift_card_data.xml new file mode 100644 index 00000000000..4ec5db91191 --- /dev/null +++ b/addons/pos_gift_card/data/gift_card_data.xml @@ -0,0 +1,54 @@ + + + + + + True + + + + Gift Card + gift.card + qweb-pdf + pos_gift_card.gift_card_template + report + + + + + + diff --git a/addons/pos_gift_card/models/__init__.py b/addons/pos_gift_card/models/__init__.py new file mode 100644 index 00000000000..93c9b33a1cb --- /dev/null +++ b/addons/pos_gift_card/models/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import barcode_rule +from . import gift_card +from . import pos_config +from . import pos_order +from . import pos_order_line diff --git a/addons/pos_gift_card/models/barcode_rule.py b/addons/pos_gift_card/models/barcode_rule.py new file mode 100644 index 00000000000..bf4a97eec01 --- /dev/null +++ b/addons/pos_gift_card/models/barcode_rule.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields + + +class BarcodeRule(models.Model): + _inherit = 'barcode.rule' + + type = fields.Selection(selection_add=[ + ('gift_card', 'Gift Card'), + ], ondelete={ + 'gift_card': 'set default', + }) diff --git a/addons/pos_gift_card/models/gift_card.py b/addons/pos_gift_card/models/gift_card.py new file mode 100644 index 00000000000..0f54772179d --- /dev/null +++ b/addons/pos_gift_card/models/gift_card.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class GiftCard(models.Model): + _inherit = "gift.card" + + buy_pos_order_line_id = fields.Many2one( + "pos.order.line", + copy=False, + readonly=True, + help="Pos Order line where this gift card has been bought.", + ) + redeem_pos_order_line_ids = fields.One2many( + "pos.order.line", "gift_card_id", string="Pos Redeems" + ) + + @api.depends("redeem_pos_order_line_ids") + def _compute_balance(self): + super()._compute_balance() + for record in self: + confirmed_line = record.redeem_pos_order_line_ids.filtered( + lambda l: l.order_id.state == "paid" + ) + balance = record.balance + if confirmed_line: + balance -= sum( + confirmed_line.mapped( + lambda line: line.currency_id._convert( + line.price_unit, + record.currency_id, + record.env.company, + line.create_date, + ) + * -1 + ) + ) + record.balance = balance + + def can_be_used_in_pos(self): + # expired state are computed once a day, so can be not synchro + return self.state == 'valid' and self.balance > 0 and self.expired_date >= fields.Date.today() and self.buy_pos_order_line_id + + def can_be_used(self): + return super(GiftCard, self).can_be_used() and not self.buy_pos_order_line_id diff --git a/addons/pos_gift_card/models/pos_config.py b/addons/pos_gift_card/models/pos_config.py new file mode 100644 index 00000000000..ef55d864364 --- /dev/null +++ b/addons/pos_gift_card/models/pos_config.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class PosConfig(models.Model): + _inherit = "pos.config" + + use_gift_card = fields.Boolean(string="Gift Card") + + gift_card_product_id = fields.Many2one( + "product.product", + string="Gift Card Product", + help="This product is used as reference on customer receipts.", + ) + + gift_card_settings = fields.Selection( + [ + ("create_set", "Generate a new barcode and set a price"), + ("scan_set", "Scan an existing barcode and set a price"), + ("scan_use", "Scan an existing barcode with an existing price"), + ], + string="Gift Cards settings", + default="create_set", + help="Defines the way you want to set your gift cards.", + ) + + @api.onchange("use_gift_card") + def _onchange_giftproduct(self): + if self.use_gift_card: + self.gift_card_product_id = self.env.ref( + "gift_card.pay_with_gift_card_product", False + ) + else: + self.gift_card_product_id = False diff --git a/addons/pos_gift_card/models/pos_order.py b/addons/pos_gift_card/models/pos_order.py new file mode 100644 index 00000000000..dace6d2a7da --- /dev/null +++ b/addons/pos_gift_card/models/pos_order.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models +import base64 + + +class PosOrder(models.Model): + _inherit = "pos.order" + + gift_card_count = fields.Integer(compute="_compute_gift_card_count") + + @api.depends("lines.generated_gift_card_ids") + def _compute_gift_card_count(self): + for record in self: + record.gift_card_count = len(record.lines.mapped("generated_gift_card_ids")) + + @api.model + def create_from_ui(self, orders, draft=False): + order_ids = super(PosOrder, self).create_from_ui(orders, draft) + for order in self.sudo().browse([o["id"] for o in order_ids]): + gift_card_config = order.config_id.gift_card_settings + for line in order.lines: + if line.product_id.id == order.config_id.gift_card_product_id.id: + if not line.gift_card_id: + if gift_card_config == "create_set": + new_card = line._create_gift_cards() + new_card.partner_id = order.partner_id or False + line.generated_gift_card_ids = new_card + else: + gift_card = self.env["gift.card"].search( + [("id", "=", line.generated_gift_card_ids.id)] + ) + gift_card.buy_pos_order_line_id = line.id + gift_card.expired_date = fields.Date.add( + fields.Date.today(), years=1 + ) + gift_card.partner_id = order.partner_id or False + + if gift_card_config == "scan_set": + gift_card.initial_amount = line.price_unit + + return order_ids + + def get_new_card_ids(self): + return self.lines.mapped("generated_gift_card_ids").ids + + def _add_mail_attachment(self, name, ticket): + attachment = super()._add_mail_attachment(name, ticket) + if self.config_id.use_gift_card and len(self.get_new_card_ids()) > 0: + report = self.env.ref('pos_gift_card.gift_card_report_pdf')._render_qweb_pdf(self.get_new_card_ids()) + filename = name + '.pdf' + gift_card = self.env['ir.attachment'].create({ + 'name': filename, + 'type': 'binary', + 'datas': base64.b64encode(report[0]), + 'store_fname': filename, + 'res_model': 'pos.order', + 'res_id': self.ids[0], + 'mimetype': 'application/x-pdf' + }) + attachment += [(4, gift_card.id)] + + return attachment diff --git a/addons/pos_gift_card/models/pos_order_line.py b/addons/pos_gift_card/models/pos_order_line.py new file mode 100644 index 00000000000..0f4bc9e9eb7 --- /dev/null +++ b/addons/pos_gift_card/models/pos_order_line.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class PosOrderLine(models.Model): + _inherit = "pos.order.line" + + generated_gift_card_ids = fields.One2many( + "gift.card", "buy_pos_order_line_id", string="Bought Gift Card" + ) + gift_card_id = fields.Many2one( + "gift.card", help="Deducted from this Gift Card", copy=False + ) + + def _is_not_sellable_line(self): + return self.gift_card_id or super()._is_not_sellable_line() + + def _create_gift_cards(self): + return self.env["gift.card"].create( + [self._build_gift_card() for _ in range(int(self.qty))] + ) + + def _build_gift_card(self): + return { + "initial_amount": self.order_id.currency_id._convert( + self.price_unit, + self.company_id.currency_id, + self.company_id, + fields.Date.today(), + ), + "buy_pos_order_line_id": self.id, + } diff --git a/addons/pos_gift_card/security/ir.model.access.csv b/addons/pos_gift_card/security/ir.model.access.csv new file mode 100644 index 00000000000..1468d9216f2 --- /dev/null +++ b/addons/pos_gift_card/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_gift_card_sales,Gift Card Program Salesman,model_gift_card,point_of_sale.group_pos_manager,1,0,0,0 +access_pos_gift_card_manager,Gift Card Program Sale Manager,model_gift_card,point_of_sale.group_pos_manager,1,1,1,1 diff --git a/addons/pos_gift_card/static/src/css/giftCard.css b/addons/pos_gift_card/static/src/css/giftCard.css new file mode 100644 index 00000000000..9332b7a1a2b --- /dev/null +++ b/addons/pos_gift_card/static/src/css/giftCard.css @@ -0,0 +1,45 @@ +.giftCardPopupMain { + display: flex; + flex-wrap: wrap; + + justify-content: space-around; + + text-align: center; + padding: 1em; +} + +.giftCardPopupContainer { + display: flex; + justify-content: center; + flex-wrap: wrap; + padding: 5px; + + font-weight: bold; + text-align: center; + width: 100%; +} + +.giftCardPopupFooter { + display: flex; + justify-content: space-evenly; + + text-align: center; +} + +.giftCardPopupInput { + border-bottom: 1px solid !important; + background-color: #F0EEEE !important; + box-shadow: none !important; + text-align: right !important; +} + +.giftCardPopupButton { + width: 50% !important; + margin: 6px; +} + +.giftCardPopupConfirmButton { + width: 40% !important; +} + +.giftCardPopupIn diff --git a/addons/pos_gift_card/static/src/js/GiftCardButton.js b/addons/pos_gift_card/static/src/js/GiftCardButton.js new file mode 100644 index 00000000000..a8782865291 --- /dev/null +++ b/addons/pos_gift_card/static/src/js/GiftCardButton.js @@ -0,0 +1,31 @@ +odoo.define("pos_gift_card.GiftCardButton", function (require) { + "use strict"; + + const PosComponent = require("point_of_sale.PosComponent"); + const ProductScreen = require("point_of_sale.ProductScreen"); + const { useListener } = require("web.custom_hooks"); + const Registries = require("point_of_sale.Registries"); + const { Gui } = require("point_of_sale.Gui"); + + class GiftCardButton extends PosComponent { + constructor() { + super(...arguments); + useListener("click", this.onClick); + } + async onClick() { + this.showPopup("GiftCardPopup", {}); + } + } + GiftCardButton.template = "GiftCardButton"; + + ProductScreen.addControlButton({ + component: GiftCardButton, + condition: function () { + return this.env.pos.config.use_gift_card; + }, + }); + + Registries.Component.add(GiftCardButton); + + return GiftCardButton; +}); diff --git a/addons/pos_gift_card/static/src/js/GiftCardPopup.js b/addons/pos_gift_card/static/src/js/GiftCardPopup.js new file mode 100644 index 00000000000..1adfdbb919f --- /dev/null +++ b/addons/pos_gift_card/static/src/js/GiftCardPopup.js @@ -0,0 +1,151 @@ +odoo.define("pos_gift_card.GiftCardPopup", function (require) { + "use strict"; + + const { useState, useRef } = owl.hooks; + const AbstractAwaitablePopup = require("point_of_sale.AbstractAwaitablePopup"); + const Registries = require("point_of_sale.Registries"); + + class GiftCardPopup extends AbstractAwaitablePopup { + constructor() { + super(...arguments); + this.state = useState({ + giftCardConfig: this.env.pos.config.gift_card_settings, + showBarcodeGeneration: false, + showNewGiftCardMenu: false, + showUseGiftCardMenu: false, + showGiftCardDetails: false, + amountToSet: 0, + giftCardBarcode: "", + }); + } + + switchBarcodeView() { + this.state.showBarcodeGeneration = !this.state.showBarcodeGeneration; + if (this.state.showUseGiftCardMenu) + this.state.showUseGiftCardMenu = false; + if (this.state.showGiftCardDetails) + this.state.showGiftCardDetails = false; + } + + switchToUseGiftCardMenu() { + this.switchBarcodeView(); + this.state.showUseGiftCardMenu = true; + } + + switchToShowGiftCardDetails() { + this.switchBarcodeView(); + this.state.showGiftCardDetails = true; + } + + addGiftCardProduct(giftCard) { + let gift = + this.env.pos.db.product_by_id[ + this.env.pos.config.gift_card_product_id[0] + ]; + this.env.pos.get_order().add_product(gift, { + price: this.state.amountToSet, + quantity: 1, + merge: false, + generated_gift_card_ids: giftCard ? giftCard.id : false, + }); + } + + async getGiftCard() { + if (this.state.giftCardBarcode == "") return; + + let giftCard = this.env.pos.giftCard.find( + (gift) => gift.code === this.state.giftCardBarcode + ); + + if (!giftCard) { + giftCard = await this.rpc({ + model: "gift.card", + method: "search_read", + args: [[["code", "=", this.state.giftCardBarcode]]], + fields: ["code", "initial_amount", "balance"], + }); + if (giftCard.length) { + this.env.pos.giftCard.push(giftCard[0]) + giftCard = giftCard[0]; + } else { + return false; + } + } + + return giftCard; + } + + async scanAndUseGiftCard() { + let giftCard = await this.getGiftCard(); + if (!giftCard) return; + + if (this.state.giftCardConfig === "scan_use") + this.state.amountToSet = giftCard.initial_amount; + + this.addGiftCardProduct(giftCard); + this.cancel(); + } + + async generateBarcode() { + this.addGiftCardProduct(false); + this.confirm(); + } + + async isGiftCardAlreadyUsed() { + let order = this.env.pos.get_order(); + let giftProduct = + this.env.pos.db.product_by_id[ + this.env.pos.config.gift_card_product_id[0] + ]; + + for (let line of order.orderlines.models) { + if (line.product.id === giftProduct.id && line.price < 0) { + if (line.gift_card_id === await this.getGiftCard().id) return line; + } + } + return false; + } + + getPriceToRemove(giftCard) { + let currentOrder = this.env.pos.get_order(); + return currentOrder.get_total_with_tax() > giftCard.balance + ? -giftCard.balance + : -currentOrder.get_total_with_tax(); + } + + async payWithGiftCard() { + let giftCard = await this.getGiftCard(); + if (!giftCard) return; + + let gift = + this.env.pos.db.product_by_id[ + this.env.pos.config.gift_card_product_id[0] + ]; + + let currentOrder = this.env.pos.get_order(); + let lineUsed = this.isGiftCardAlreadyUsed() + if (lineUsed) currentOrder.remove_orderline(lineUsed); + + currentOrder.add_product(gift, { + price: this.getPriceToRemove(giftCard), + quantity: 1, + merge: false, + gift_card_id: giftCard.id, + }); + + this.cancel(); + } + + async ShowRemainingAmount() { + let giftCard = await this.getGiftCard(); + if (!giftCard) return; + + this.state.amountToSet = giftCard.balance; + } + } + GiftCardPopup.template = "GiftCardPopup"; + + Registries.Component.add(GiftCardPopup); + + return GiftCardPopup; +}); diff --git a/addons/pos_gift_card/static/src/js/PaymentScreen.js b/addons/pos_gift_card/static/src/js/PaymentScreen.js new file mode 100644 index 00000000000..edb3d0f516e --- /dev/null +++ b/addons/pos_gift_card/static/src/js/PaymentScreen.js @@ -0,0 +1,76 @@ +odoo.define('pos_gift_card.PaymentScreen', function(require) { + "use strict"; + + const PaymentScreen = require('point_of_sale.PaymentScreen'); + const Registries = require('point_of_sale.Registries'); + var core = require('web.core'); + var _t = core._t; + + + const PosGiftCardPaymentScreen = PaymentScreen => class extends PaymentScreen { + //@Override + async validateOrder(isForceValidate) { + if(this.env.pos.config.use_gift_card) { + if (await this._isOrderValid(isForceValidate)) { + try { + let giftProduct = this.env.pos.db.product_by_id[this.env.pos.config.gift_card_product_id[0]]; + + for (let line of this.currentOrder.orderlines.models) { + if(line.product.id === giftProduct.id && line.price <= 0) { + let is_valid = await this.rpc({ + model: "gift.card", + method: 'can_be_used_in_pos', + args: [line.gift_card_id], + }); + + if(!is_valid) { + await this.showPopup('ErrorPopup', { + 'title': _t("Gift Card Error"), + 'body': _t("Gift card is not valid."), + }); + return; + } + + let gift_card = await this.rpc({ + model: "gift.card", + method: 'search_read', + domain: [['id', '=', line.gift_card_id]], + fields: ['balance'], + }); + + if(Math.abs(line.price) > gift_card[0].balance) { + await this.showPopup('ErrorPopup', { + 'title': _t("Gift Card Error"), + 'body': _t("Gift card balance is too low."), + }); + return; + } + this.env.pos.giftCard.find(gift => gift.id === gift_card[0].id).balance += line.price; + } + } + } catch (e) { + // do nothing with the error + } + } + } + await super.validateOrder(...arguments); + } + + async _postPushOrderResolve(order, server_ids) { + if(this.env.pos.config.use_gift_card) { + let ids = await this.rpc({ + model: 'pos.order', + method: 'get_new_card_ids', + args: [server_ids] + }); + if(ids.length > 0) + this.env.pos.print_gift_pdf(ids); + } + return super._postPushOrderResolve(order, server_ids); + } + }; + + Registries.Component.extend(PaymentScreen, PosGiftCardPaymentScreen); + + return PosGiftCardPaymentScreen; +}); diff --git a/addons/pos_gift_card/static/src/js/models.js b/addons/pos_gift_card/static/src/js/models.js new file mode 100644 index 00000000000..609b0842593 --- /dev/null +++ b/addons/pos_gift_card/static/src/js/models.js @@ -0,0 +1,86 @@ +odoo.define("pos_gift_card.gift_card", function (require) { + "use strict"; + + const models = require("point_of_sale.models"); + + models.load_fields("pos.order.line", "generated_gift_card_ids"); + models.load_fields("pos.order.line", "redeem_pos_order_line_ids"); + + // Load the products used for creating program reward lines. + var existing_models = models.PosModel.prototype.models; + var product_index = _.findIndex(existing_models, function (model) { + return model.model === 'product.product'; + }); + var product_model = existing_models[product_index]; + + models.load_models([ + { + model: "gift.card", + fields: ["code", "initial_amount", "balance"], + loaded: function (self, giftCard) { + self.giftCard = giftCard; + }, + }, { + model: product_model.model, + fields: product_model.fields, + order: product_model.order, + domain: function (self) { + return [['id', '=', self.config.gift_card_product_id[0]]]; + }, + context: product_model.context, + loaded: product_model.loaded, + }, + ]); + + var _order_super = models.Order.prototype; + models.Order = models.Order.extend({ + set_orderline_options: function (orderline, options) { + _order_super.set_orderline_options.apply(this, [orderline, options]); + if (options && options.generated_gift_card_ids) { + orderline.generated_gift_card_ids = [options.generated_gift_card_ids]; + } + if (options && options.gift_card_id) { + orderline.gift_card_id = options.gift_card_id; + } + }, + wait_for_push_order: function () { + if(this.pos.config.use_gift_card) { + let giftProduct = this.pos.db.product_by_id[this.pos.config.gift_card_product_id[0]]; + for (let line of this.orderlines.models) { + if(line.product.id === giftProduct.id) + return true; + } + } + return _order_super.wait_for_push_order.apply(this, arguments); + }, + }); + + var _super_orderline = models.Orderline; + models.Orderline = models.Orderline.extend({ + export_as_JSON: function () { + var json = _super_orderline.prototype.export_as_JSON.apply( + this, + arguments + ); + json.generated_gift_card_ids = this.generated_gift_card_ids; + json.gift_card_id = this.gift_card_id; + return json; + }, + init_from_JSON: function (json) { + _super_orderline.prototype.init_from_JSON.apply(this, arguments); + this.generated_gift_card_ids = json.generated_gift_card_ids; + this.gift_card_id = json.gift_card_id; + }, + }); + + var _posmodel_super = models.PosModel.prototype; + models.PosModel = models.PosModel.extend({ + print_gift_pdf: function (giftCardIds) { + this.do_action('pos_gift_card.gift_card_report_pdf', { + additional_context: { + active_ids: [giftCardIds], + }, + }) + } + }); +}); diff --git a/addons/pos_gift_card/static/src/xml/GiftCardButton.xml b/addons/pos_gift_card/static/src/xml/GiftCardButton.xml new file mode 100644 index 00000000000..0ac7c533ac0 --- /dev/null +++ b/addons/pos_gift_card/static/src/xml/GiftCardButton.xml @@ -0,0 +1,12 @@ + + + + + + + + Gift Card + + + + diff --git a/addons/pos_gift_card/static/src/xml/GiftCardPopup.xml b/addons/pos_gift_card/static/src/xml/GiftCardPopup.xml new file mode 100644 index 00000000000..a61f3a7e0cf --- /dev/null +++ b/addons/pos_gift_card/static/src/xml/GiftCardPopup.xml @@ -0,0 +1,140 @@ + + + + + + + + diff --git a/addons/pos_gift_card/views/gift_card_views.xml b/addons/pos_gift_card/views/gift_card_views.xml new file mode 100644 index 00000000000..8904e07c1fd --- /dev/null +++ b/addons/pos_gift_card/views/gift_card_views.xml @@ -0,0 +1,33 @@ + + + + + + gift.card.form Website + gift.card + + + + + + + + + + + + + + + + + + + diff --git a/addons/pos_gift_card/views/pos_config_views.xml b/addons/pos_gift_card/views/pos_config_views.xml new file mode 100644 index 00000000000..9ae52ae0a82 --- /dev/null +++ b/addons/pos_gift_card/views/pos_config_views.xml @@ -0,0 +1,29 @@ + + + + pos.config.form + pos.config + + + +
+
+ +
+
+
+
+
+
+
+
diff --git a/addons/pos_gift_card/views/res_config_settings_views.xml b/addons/pos_gift_card/views/res_config_settings_views.xml new file mode 100644 index 00000000000..702b619466d --- /dev/null +++ b/addons/pos_gift_card/views/res_config_settings_views.xml @@ -0,0 +1,15 @@ + + + + res.config.form.inherit.pos.coupon + res.config.settings + + + +
+
+
+
+
+
diff --git a/addons/sale_gift_card/__init__.py b/addons/sale_gift_card/__init__.py new file mode 100644 index 00000000000..dc5e6b693d1 --- /dev/null +++ b/addons/sale_gift_card/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models diff --git a/addons/sale_gift_card/__manifest__.py b/addons/sale_gift_card/__manifest__.py new file mode 100644 index 00000000000..328f215e89e --- /dev/null +++ b/addons/sale_gift_card/__manifest__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': "Gift Card for sales module", + 'summary': "Use gift card in your sales orders", + 'description': """Integrate gift card mechanism in sales orders.""", + 'category': 'Sales/Sales', + 'version': '1.0', + 'depends': ['gift_card', 'sale'], + 'auto_install': True, + 'data': [ + 'data/gift_card_data.xml', + 'data/mail_template_data.xml', + 'views/sale_order_view.xml', + 'views/templates.xml', + 'security/ir.model.access.csv', + ], + 'license': 'LGPL-3', +} diff --git a/addons/sale_gift_card/data/gift_card_data.xml b/addons/sale_gift_card/data/gift_card_data.xml new file mode 100644 index 00000000000..1830684c64e --- /dev/null +++ b/addons/sale_gift_card/data/gift_card_data.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/addons/gift_card/data/mail_template_data.xml b/addons/sale_gift_card/data/mail_template_data.xml similarity index 100% rename from addons/gift_card/data/mail_template_data.xml rename to addons/sale_gift_card/data/mail_template_data.xml diff --git a/addons/sale_gift_card/models/__init__.py b/addons/sale_gift_card/models/__init__.py new file mode 100644 index 00000000000..733bcda9ea8 --- /dev/null +++ b/addons/sale_gift_card/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import gift_card +from . import sale_order diff --git a/addons/sale_gift_card/models/gift_card.py b/addons/sale_gift_card/models/gift_card.py new file mode 100644 index 00000000000..1949f03e3dd --- /dev/null +++ b/addons/sale_gift_card/models/gift_card.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, fields, models + + +class GiftCard(models.Model): + _inherit = "gift.card" + + buy_line_id = fields.Many2one("sale.order.line", copy=False, readonly=True, + help="Sale Order line where this gift card has been bought.") + redeem_line_ids = fields.One2many('sale.order.line', 'gift_card_id', string="Redeems") + + @api.depends("redeem_line_ids") + def _compute_balance(self): + super()._compute_balance() + for record in self: + confirmed_line = record.redeem_line_ids.filtered(lambda l: l.state == 'sale') + balance = record.balance + if confirmed_line: + balance -= sum(confirmed_line.mapped( + lambda line: line.currency_id._convert(line.price_unit, record.currency_id, record.env.company, line.create_date) * -1 + )) + record.balance = balance diff --git a/addons/gift_card/models/sale_order.py b/addons/sale_gift_card/models/sale_order.py similarity index 97% rename from addons/gift_card/models/sale_order.py rename to addons/sale_gift_card/models/sale_order.py index 8329b6fc14a..9c0fbc53d1d 100644 --- a/addons/gift_card/models/sale_order.py +++ b/addons/sale_gift_card/models/sale_order.py @@ -51,7 +51,7 @@ class SaleOrder(models.Model): return error def _send_gift_card_mail(self): - template = self.env.ref('gift_card.mail_template_gift_card', raise_if_not_found=False) + template = self.env.ref('sale_gift_card.mail_template_gift_card', raise_if_not_found=False) if template and self.gift_card_count: for gift in self.order_line.mapped("generated_gift_card_ids"): template.send_mail(gift.id, force_send=True, notif_layout='mail.mail_notification_light') diff --git a/addons/sale_gift_card/security/ir.model.access.csv b/addons/sale_gift_card/security/ir.model.access.csv new file mode 100644 index 00000000000..99bb48e912b --- /dev/null +++ b/addons/sale_gift_card/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_gift_card_sales,Gift Card Program Salesman,model_gift_card,sales_team.group_sale_salesman,1,0,0,0 +access_gift_card_manager,Gift Card Program Sale Manager,model_gift_card,sales_team.group_sale_manager,1,1,1,1 diff --git a/addons/gift_card/tests/__init__.py b/addons/sale_gift_card/tests/__init__.py similarity index 100% rename from addons/gift_card/tests/__init__.py rename to addons/sale_gift_card/tests/__init__.py diff --git a/addons/gift_card/tests/common.py b/addons/sale_gift_card/tests/common.py similarity index 100% rename from addons/gift_card/tests/common.py rename to addons/sale_gift_card/tests/common.py diff --git a/addons/gift_card/tests/test_buy_gift_card.py b/addons/sale_gift_card/tests/test_buy_gift_card.py similarity index 96% rename from addons/gift_card/tests/test_buy_gift_card.py rename to addons/sale_gift_card/tests/test_buy_gift_card.py index 9c26ff425a1..c3593ec6192 100644 --- a/addons/gift_card/tests/test_buy_gift_card.py +++ b/addons/sale_gift_card/tests/test_buy_gift_card.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import Command -from odoo.addons.gift_card.tests.common import TestSaleGiftCardCommon +from odoo.addons.sale_gift_card.tests.common import TestSaleGiftCardCommon class TestBuyGiftCard(TestSaleGiftCardCommon): diff --git a/addons/gift_card/tests/test_pay_with_gift_card.py b/addons/sale_gift_card/tests/test_pay_with_gift_card.py similarity index 96% rename from addons/gift_card/tests/test_pay_with_gift_card.py rename to addons/sale_gift_card/tests/test_pay_with_gift_card.py index 1a96561f605..c92cd5df128 100644 --- a/addons/gift_card/tests/test_pay_with_gift_card.py +++ b/addons/sale_gift_card/tests/test_pay_with_gift_card.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from odoo import Command -from odoo.addons.gift_card.tests.common import TestSaleGiftCardCommon +from odoo.addons.sale_gift_card.tests.common import TestSaleGiftCardCommon from odoo.exceptions import UserError class TestPayWithGiftCard(TestSaleGiftCardCommon): diff --git a/addons/sale_gift_card/views/sale_order_view.xml b/addons/sale_gift_card/views/sale_order_view.xml new file mode 100644 index 00000000000..4c5afdab155 --- /dev/null +++ b/addons/sale_gift_card/views/sale_order_view.xml @@ -0,0 +1,47 @@ + + + + + Gift Cards + gift.card + tree,form + [('buy_line_id.order_id', '=', active_id)] + + + + sale.order.view.form.inherit.gift.card + sale.order + + + + + + + + + + gift.card.form Website + gift.card + + + + + + + + + + + + + + + + + diff --git a/addons/gift_card/views/templates.xml b/addons/sale_gift_card/views/templates.xml similarity index 91% rename from addons/gift_card/views/templates.xml rename to addons/sale_gift_card/views/templates.xml index b69f1ad6a7a..45f0e65051b 100644 --- a/addons/gift_card/views/templates.xml +++ b/addons/sale_gift_card/views/templates.xml @@ -1,7 +1,7 @@ -