From 809f0bbe1b05f15da15dac1bfaaa583fcffdbda4 Mon Sep 17 00:00:00 2001 From: Robin Heinz Date: Mon, 16 Aug 2021 13:02:12 +0000 Subject: [PATCH] [IMP] Gift Card: Allow usage of gift card in POS Split initial module gift card into 2 modules: - gift card - sales gift card This change is made to isolate the sale order from the gift card module. In order to create the gift card in the point of sale, we added 3 options in the pos config. Create and set: generate and set the amount of a new gift card. Use and set: We use an already existing card and set the amount of this one. Use: We use a gift card that already has a certain amount. When you pay with a gift cards, reports are generated. Those reports give you the code and the barcode of the gift cards. You also get information such as expiration date and the amount. closes odoo/odoo#75518 Task-id: 1737 Related: odoo/upgrade#2799 Signed-off-by: pimodoo --- addons/gift_card/__manifest__.py | 8 +- addons/gift_card/data/gift_card_data.xml | 3 - addons/gift_card/models/__init__.py | 1 - addons/gift_card/models/gift_card.py | 24 +-- addons/gift_card/security/ir.model.access.csv | 2 - addons/gift_card/views/views.xml | 34 +--- addons/point_of_sale/models/pos_order.py | 40 +++-- .../models/res_config_settings.py | 1 + .../views/res_config_settings_views.xml | 11 ++ addons/pos_gift_card/__init__.py | 4 + addons/pos_gift_card/__manifest__.py | 32 ++++ addons/pos_gift_card/data/gift_card_data.xml | 54 +++++++ addons/pos_gift_card/models/__init__.py | 8 + addons/pos_gift_card/models/barcode_rule.py | 14 ++ addons/pos_gift_card/models/gift_card.py | 47 ++++++ addons/pos_gift_card/models/pos_config.py | 36 +++++ addons/pos_gift_card/models/pos_order.py | 64 ++++++++ addons/pos_gift_card/models/pos_order_line.py | 34 ++++ .../security/ir.model.access.csv | 3 + .../pos_gift_card/static/src/css/giftCard.css | 45 ++++++ .../static/src/js/GiftCardButton.js | 31 ++++ .../static/src/js/GiftCardPopup.js | 151 ++++++++++++++++++ .../static/src/js/PaymentScreen.js | 76 +++++++++ addons/pos_gift_card/static/src/js/models.js | 86 ++++++++++ .../static/src/xml/GiftCardButton.xml | 12 ++ .../static/src/xml/GiftCardPopup.xml | 140 ++++++++++++++++ .../pos_gift_card/views/gift_card_views.xml | 33 ++++ .../pos_gift_card/views/pos_config_views.xml | 29 ++++ .../views/res_config_settings_views.xml | 15 ++ addons/sale_gift_card/__init__.py | 4 + addons/sale_gift_card/__manifest__.py | 20 +++ addons/sale_gift_card/data/gift_card_data.xml | 14 ++ .../data/mail_template_data.xml | 0 addons/sale_gift_card/models/__init__.py | 5 + addons/sale_gift_card/models/gift_card.py | 24 +++ .../models/sale_order.py | 2 +- .../security/ir.model.access.csv | 3 + .../tests/__init__.py | 0 .../tests/common.py | 0 .../tests/test_buy_gift_card.py | 2 +- .../tests/test_pay_with_gift_card.py | 2 +- .../sale_gift_card/views/sale_order_view.xml | 47 ++++++ .../views/templates.xml | 6 +- addons/website_sale_gift_card/__manifest__.py | 2 +- .../website_sale_gift_card/views/template.xml | 6 +- 45 files changed, 1087 insertions(+), 88 deletions(-) create mode 100644 addons/pos_gift_card/__init__.py create mode 100644 addons/pos_gift_card/__manifest__.py create mode 100644 addons/pos_gift_card/data/gift_card_data.xml create mode 100644 addons/pos_gift_card/models/__init__.py create mode 100644 addons/pos_gift_card/models/barcode_rule.py create mode 100644 addons/pos_gift_card/models/gift_card.py create mode 100644 addons/pos_gift_card/models/pos_config.py create mode 100644 addons/pos_gift_card/models/pos_order.py create mode 100644 addons/pos_gift_card/models/pos_order_line.py create mode 100644 addons/pos_gift_card/security/ir.model.access.csv create mode 100644 addons/pos_gift_card/static/src/css/giftCard.css create mode 100644 addons/pos_gift_card/static/src/js/GiftCardButton.js create mode 100644 addons/pos_gift_card/static/src/js/GiftCardPopup.js create mode 100644 addons/pos_gift_card/static/src/js/PaymentScreen.js create mode 100644 addons/pos_gift_card/static/src/js/models.js create mode 100644 addons/pos_gift_card/static/src/xml/GiftCardButton.xml create mode 100644 addons/pos_gift_card/static/src/xml/GiftCardPopup.xml create mode 100644 addons/pos_gift_card/views/gift_card_views.xml create mode 100644 addons/pos_gift_card/views/pos_config_views.xml create mode 100644 addons/pos_gift_card/views/res_config_settings_views.xml create mode 100644 addons/sale_gift_card/__init__.py create mode 100644 addons/sale_gift_card/__manifest__.py create mode 100644 addons/sale_gift_card/data/gift_card_data.xml rename addons/{gift_card => sale_gift_card}/data/mail_template_data.xml (100%) create mode 100644 addons/sale_gift_card/models/__init__.py create mode 100644 addons/sale_gift_card/models/gift_card.py rename addons/{gift_card => sale_gift_card}/models/sale_order.py (97%) create mode 100644 addons/sale_gift_card/security/ir.model.access.csv rename addons/{gift_card => sale_gift_card}/tests/__init__.py (100%) rename addons/{gift_card => sale_gift_card}/tests/common.py (100%) rename addons/{gift_card => sale_gift_card}/tests/test_buy_gift_card.py (96%) rename addons/{gift_card => sale_gift_card}/tests/test_pay_with_gift_card.py (96%) create mode 100644 addons/sale_gift_card/views/sale_order_view.xml rename addons/{gift_card => sale_gift_card}/views/templates.xml (91%) 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 @@ -