[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 <pimodoo@users.noreply.github.com>
This commit is contained in:
Robin Heinz
2021-09-03 10:37:54 +00:00
committed by Pierre Masereel
parent ff4d25a427
commit 809f0bbe1b
45 changed files with 1087 additions and 88 deletions
+3 -5
View File
@@ -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',
-3
View File
@@ -8,8 +8,6 @@
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="sale_ok" eval="False"/>
<field name="taxes_id" eval="False"/>
<field name="supplier_taxes_id" eval="False"/>
<field name="image_1920" type="base64" file="gift_card/static/img/gift_card.png"/>
</record>
@@ -20,7 +18,6 @@
<field name="type">service</field>
<field name="purchase_ok" eval="False"/>
<field name="is_gift_card" eval="True"/>
<field name="taxes_id" eval="False"/>
<field name="image_1920" type="base64" file="gift_card/static/img/gift_card.png"/>
</record>
</data>
-1
View File
@@ -3,4 +3,3 @@
from . import gift_card
from . import product
from . import sale_order
+7 -17
View File
@@ -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
@@ -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
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 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
+1 -33
View File
@@ -1,13 +1,6 @@
<?xml version="1.0"?>
<odoo>
<!-- Actions -->
<record id="gift_card_sale_order_action" model="ir.actions.act_window">
<field name="name">Gift Cards</field>
<field name="res_model">gift.card</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('buy_line_id.order_id', '=', active_id)]</field>
</record>
<record id="gift_card_action" model="ir.actions.act_window">
<field name="name">Gift Cards</field>
<field name="res_model">gift.card</field>
@@ -39,25 +32,17 @@
<sheet>
<group>
<group>
<field name="buy_line_id" string="Sale Order"/>
<field name="create_date"/>
<field name="expired_date"/>
<field name="code"/>
</group>
<group>
<group name="gift_card">
<field name="currency_id" attrs="{'invisible': True}"/>
<field name="initial_amount"/>
<field name="balance"/>
<field name="partner_id"/>
</group>
</group>
<group>
<field name="redeem_line_ids" options="{'no_create': True}" readonly="1">
<tree>
<field name="order_id"/>
</tree>
</field>
</group>
</sheet>
</form>
</field>
@@ -74,21 +59,4 @@
</search>
</field>
</record>
<record id="sale_order_view_extend_gift_card_form" model="ir.ui.view">
<field name="name">sale.order.view.form.inherit.gift.card</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='preview_sale_order']" position="before">
<button class="oe_stat_button"
name="%(gift_card_sale_order_action)d"
attrs="{'invisible': [('gift_card_count', '=', 0)]}"
icon="fa-gift"
type="action">
<field name="gift_card_count" widget="statinfo" string="Gift Cards"/>
</button>
</xpath>
</field>
</record>
</odoo>
+23 -17
View File
@@ -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 = _("<p>Dear %s,<br/>Here is your electronic ticket for the %s. </p>") % (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 = _("<p>Dear %s,<br/>Here is your electronic ticket for the %s. </p>") % (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()
@@ -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()
@@ -77,6 +77,17 @@
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box" title="Manage gift card.">
<div class="o_setting_left_pane">
<field name="module_pos_gift_card"/>
</div>
<div class="o_setting_right_pane" id="pos-gift-card">
<label for="module_pos_gift_card" string="Gift card"/>
<div class="text-muted">
Manage gift card
</div>
</div>
</div>
</div>
<h2>Payments</h2>
<div class="row mt16 o_settings_container">
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+32
View File
@@ -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',
}
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- the product used to deduce on sale order -->
<record id="gift_card.pay_with_gift_card_product" model="product.product">
<field name="available_in_pos">True</field>
</record>
<record id="gift_card_report_pdf" model="ir.actions.report">
<field name="name">Gift Card</field>
<field name="model">gift.card</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">pos_gift_card.gift_card_template</field>
<field name="binding_type">report</field>
<field name="binding_model_id" ref="model_gift_card"/>
</record>
<template id="gift_card_template">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div style="margin:0px; font-size:24px; font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:36px; color:#333333; text-align: center">
Here is your gift card!
</div>
<div style="padding-top:20px; padding-bottom:20px">
<img src="/gift_card/static/img/gift_card.png" style="display:block; border:0; outline:none; text-decoration:none; margin:auto;" width="300"/>
</div>
<div style="padding:0; margin:0px; padding-top:35px; padding-bottom:35px; text-align:center;">
<h3 style="margin:0px; line-height:48px; font-family:arial, 'helvetica neue', helvetica, sans-serif; font-size:40px; font-style:normal; font-weight:normal; color:#333333; text-align:center">
<strong><span t-field="o.initial_amount"/></strong>
</h3>
</div>
<div style="padding:0; margin:0px; padding-top:35px; padding-bottom:35px; background-color:#efefef; text-align:center;">
<p style="margin:0px; font-size:14px;font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:21px; color:#333333">
<strong>Gift Card Code</strong>
</p>
<p style="margin:0px; font-size:25px;font-family:arial, 'helvetica neue', helvetica, sans-serif; line-height:38px; color:#A9A9A9">
<span t-field="o.code"/>
</p>
</div>
<div style="padding:0; margin:0px; padding-top:10px; padding-bottom:10px; text-align:center;">
<h3 style="margin:0px; line-height:17px; font-family:arial, 'helvetica neue', helvetica, sans-serif; font-size:14px; font-style:normal; font-weight:normal; color:#A9A9A9; text-align:center">
Card expires <span t-field="o.expired_date"/>
</h3>
</div>
<div style="padding:0; margin:0px; padding-top:10px; padding-bottom:10px; text-align:center;">
<img t-att-src="'/report/barcode/Code128/'+o.code" style="width:200px;height:50px" alt="Barcode"/>
</div>
</t>
</t>
</t>
</template>
</data>
</odoo>
+8
View File
@@ -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
@@ -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',
})
+47
View File
@@ -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
+36
View File
@@ -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
+64
View File
@@ -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
@@ -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,
}
@@ -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
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_gift_card_sales Gift Card Program Salesman model_gift_card point_of_sale.group_pos_manager 1 0 0 0
3 access_pos_gift_card_manager Gift Card Program Sale Manager model_gift_card point_of_sale.group_pos_manager 1 1 1 1
@@ -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
@@ -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;
});
@@ -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;
});
@@ -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;
});
@@ -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],
},
})
}
});
});
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="GiftCardButton" owl="1">
<span class="control-button">
<i class="fa fa-gift"></i>
<span> </span>
<span>Gift Card</span>
</span>
</t>
</templates>
@@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="GiftCardPopup" owl="1">
<div role="dialog" class="modal-dialog">
<Draggable>
<div class="popup popup-textarea">
<header class="title drag-handle">
Gift Card
</header>
<main class="giftCardPopupMain">
<div class="giftCardPopupContainer" t-if="!state.showBarcodeGeneration">
<span t-if="state.giftCardConfig == 'create_set'"
class="giftCardPopupButton button"
t-on-click="switchBarcodeView">Generate barcode</span>
<span t-if="state.giftCardConfig == 'scan_set'"
class="giftCardPopupButton button"
t-on-click="switchBarcodeView">Scan and set price on gift card</span>
<span t-if="state.giftCardConfig == 'scan_use'"
class="giftCardPopupButton button"
t-on-click="switchBarcodeView">Scan gift card</span>
</div>
<div t-if="!state.showUseGiftCardMenu &amp;&amp; !state.showGiftCardDetails">
<div class="giftCardPopupContainer"
t-if="state.showBarcodeGeneration &amp;&amp; state.giftCardConfig == 'create_set'">
<div>
<span>Amount of the gift card:</span><br/>
<div>
<input t-model.number="state.amountToSet" class="giftCardPopupInput" type="text"/>
<span class="currency">
<t t-esc="env.pos.getCurrencySymbol()" />
</span>
</div>
</div>
<div>
<div class="button confirm" t-on-click="generateBarcode">
Confirm
</div>
<div class="button confirm" t-on-click="switchBarcodeView">
Cancel
</div>
</div>
</div>
<div class="giftCardPopupContainer"
t-if="state.showBarcodeGeneration &amp;&amp; state.giftCardConfig == 'scan_set'">
<div>
Gift Card Barcode: <br/>
<input t-model="state.giftCardBarcode" class="giftCardPopupInput" type="text"/>
</div>
<div>
Amount of the gift card: <br/>
<input t-model.number="state.amountToSet" class="giftCardPopupInput" type="text"/>
</div>
<div>
<span class="button confirm" t-on-click="scanAndUseGiftCard">
Confirm
</span>
<span class="button cancel" t-on-click="switchBarcodeView">
Discard
</span>
</div>
</div>
<div class="giftCardPopupContainer"
t-if="state.showBarcodeGeneration &amp;&amp; state.giftCardConfig == 'scan_use'">
<div>
Gift Card Barcode:
<input t-model="state.giftCardBarcode" class="giftCardPopupInput" type="text"/>
</div>
<div>
<span class="button confirm" t-on-click="scanAndUseGiftCard">
Confirm
</span>
<span class="button cancel" t-on-click="switchBarcodeView">
Discard
</span>
</div>
</div>
</div>
<div class="giftCardPopupContainer"
t-if="state.showBarcodeGeneration &amp;&amp; state.showUseGiftCardMenu">
<div>
Gift Card Barcode:
<input t-model="state.giftCardBarcode" class="giftCardPopupInput" type="text"/>
</div>
<div>
<span class="button confirm" t-on-click="payWithGiftCard">
Confirm
</span>
<span class="button cancel" t-on-click="switchBarcodeView">
Discard
</span>
</div>
</div>
<div class="giftCardPopupContainer"
t-if="state.showBarcodeGeneration &amp;&amp; state.showGiftCardDetails">
<div>
Gift Card Barcode:
<input t-model="state.giftCardBarcode" class="giftCardPopupInput" type="text"/>
</div>
<div>
Remaining amount of the gift card:
<t t-esc="state.amountToSet"/>
</div>
<div>
<span class="button confirm" t-on-click="ShowRemainingAmount">
Confirm
</span>
<span class="button cancel" t-on-click="switchBarcodeView">
Discard
</span>
</div>
</div>
<div class="giftCardPopupContainer" t-if="!state.showBarcodeGeneration">
<div class="button giftCardPopupConfirmButton" t-on-click="switchToUseGiftCardMenu">
Use a gift card
</div>
<div class="button giftCardPopupConfirmButton" t-on-click="switchToShowGiftCardDetails">
Check a gift card
</div>
</div>
</main>
<footer class="footer giftCardPopupFooter">
<div class="button cancel" t-on-click="cancel">
Cancel
</div>
</footer>
</div>
</Draggable>
</div>
</t>
</templates>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<menuitem
id="pos_gift_card_menu"
action="gift_card.gift_card_action"
parent="point_of_sale.pos_config_menu_catalog"
name="Gift Cards"
groups="point_of_sale.group_pos_manager"
sequence="100"
/>
<record id="pos_gift_card_view_form" model="ir.ui.view">
<field name="name">gift.card.form Website</field>
<field name="model">gift.card</field>
<field name="inherit_id" ref="gift_card.gift_card_view_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='code']" position="after">
<field name="buy_pos_order_line_id" options="{'no_create': True}"/>
</xpath>
<xpath expr="//group[@name='gift_card']" position="after">
<group>
<field name="redeem_pos_order_line_ids" options="{'no_create': True}" readonly="1">
<tree>
<field name="id"/>
</tree>
</field>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="pos_gift_card_config_view_form" model="ir.ui.view">
<field name="name">pos.config.form</field>
<field name="model">pos.config</field>
<field name="inherit_id" ref="point_of_sale.pos_config_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='pos-loyalty']" position="after">
<div class="col-12 col-lg-6 o_setting_box" id="pos-coupon">
<div class="o_setting_left_pane">
<field name="use_gift_card"/>
</div>
<div class="o_setting_right_pane" title="Gift Card">
<label for="use_gift_card"/>
<div class="content-group" attrs="{'invisible': [('use_gift_card', '=', False)]}">
<div class="mt16" id="gift_card_product">
<label string="Gift Card Product" for="gift_card_product_id" class="o_light_label"/>
<field name="gift_card_product_id"/><br/>
<button name="%(gift_card.gift_card_action)d" icon="fa-arrow-right" type="action" string="Gift Card" class="btn-link"/><br/>
<label for="gift_card_settings" string="Gift card settings"/>
<field name="gift_card_settings" widget="radio"/>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_view_form_inherit_pos_coupon" model="ir.ui.view">
<field name="name">res.config.form.inherit.pos.coupon</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="point_of_sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='pos-gift-card']/div[last()]" position="after">
<div class="mt8" attrs="{'invisible': [('module_pos_gift_card', '=', False)]}">
<button name="%(gift_card.gift_card_action)d" icon="fa-arrow-right" type="action" string="Gift Card" class="btn-link"/><br/>
</div>
</xpath>
</field>
</record>
</odoo>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
+20
View File
@@ -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',
}
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- the product used to deduce on sale order -->
<record id="gift_card.pay_with_gift_card_product" model="product.product">
<field name="taxes_id" eval="False"/>
<field name="supplier_taxes_id" eval="False"/>
</record>
<record id="gift_card.gift_card_product_50" model="product.product">
<field name="taxes_id" eval="False"/>
</record>
</data>
</odoo>
+5
View File
@@ -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
+24
View File
@@ -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
@@ -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')
@@ -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
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_gift_card_sales Gift Card Program Salesman model_gift_card sales_team.group_sale_salesman 1 0 0 0
3 access_gift_card_manager Gift Card Program Sale Manager model_gift_card sales_team.group_sale_manager 1 1 1 1
@@ -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):
@@ -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):
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<odoo>
<!-- Actions -->
<record id="gift_card_sale_order_action" model="ir.actions.act_window">
<field name="name">Gift Cards</field>
<field name="res_model">gift.card</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('buy_line_id.order_id', '=', active_id)]</field>
</record>
<record id="sale_order_view_extend_gift_card_form" model="ir.ui.view">
<field name="name">sale.order.view.form.inherit.gift.card</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='preview_sale_order']" position="before">
<button class="oe_stat_button"
name="%(gift_card_sale_order_action)d"
attrs="{'invisible': [('gift_card_count', '=', 0)]}"
icon="fa-gift"
type="action">
<field name="gift_card_count" widget="statinfo" string="Gift Cards"/>
</button>
</xpath>
</field>
</record>
<record id="sale_gift_card_view_form" model="ir.ui.view">
<field name="name">gift.card.form Website</field>
<field name="model">gift.card</field>
<field name="inherit_id" ref="gift_card.gift_card_view_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='code']" position="after">
<field name="buy_line_id" options="{'no_create': True}"/>
</xpath>
<xpath expr="//group[@name='gift_card']" position="after">
<group>
<field name="redeem_line_ids" options="{'no_create': True}" readonly="1">
<tree>
<field name="order_id"/>
</tree>
</field>
</group>
</xpath>
</field>
</record>
</odoo>
@@ -1,7 +1,7 @@
<?xml version="1.0" ?>
<odoo>
<template id="gift_card.used_gift_card">
<template id="sale_gift_card.used_gift_card">
<div class="text-muted d-none d-md-block small" t-if="line.gift_card_id">
<span>
Code:
@@ -17,8 +17,8 @@
<template id="sale_order_portal_content_inherit" name="Gift Card Products Portal" inherit_id="sale.sale_order_portal_content">
<xpath expr="//section[@id='details']//td[@id='product_name']/*[last()]" position="after">
<t t-if="line.gift_card_id" t-call="gift_card.used_gift_card"/>
<t t-if="line.generated_gift_card_ids" t-call="gift_card.sale_purchased_gift_card">
<t t-if="line.gift_card_id" t-call="sale_gift_card.used_gift_card"/>
<t t-if="line.generated_gift_card_ids" t-call="sale_gift_card.sale_purchased_gift_card">
<t t-set="order" t-value="sale_order"/>
<t t-set="hide_intro" t-value="1"/>
</t>
@@ -7,7 +7,7 @@
'description': """Integrate gift card mechanism in your ecommerce.""",
'category': 'Website/Website',
'version': '1.0',
'depends': ['website_sale', 'gift_card'],
'depends': ['website_sale', 'sale_gift_card'],
'data': [
'views/template.xml',
'views/gift_card_views.xml',
@@ -25,7 +25,7 @@
</t>
<t t-else="">
<strong t-field="line.name_short"/>
<t t-call="gift_card.used_gift_card"/>
<t t-call="sale_gift_card.used_gift_card"/>
</t>
</div>
</xpath>
@@ -51,13 +51,13 @@
<template id="cart_summary_inherit_website_gift_card_sale" inherit_id="website_sale.cart_summary">
<xpath expr="//td[hasclass('td-product_name')]/div/strong" position="after">
<t t-if="line.gift_card_id" t-call="gift_card.used_gift_card"/>
<t t-if="line.gift_card_id" t-call="sale_gift_card.used_gift_card"/>
</xpath>
</template>
<template id="website_sale_purchased_gift_card" inherit_id="website_sale.confirmation" >
<xpath expr="//div[@id='oe_structure_website_sale_confirmation_2']" position="after">
<t t-call="gift_card.sale_purchased_gift_card"/>
<t t-call="sale_gift_card.sale_purchased_gift_card"/>
</xpath>
</template>
</odoo>