[MERGE] payment, website_(portal_sale/quote): separate payment and account code from portal_sale/quote

This commit separate code from website_portal_sale, website_sale and
website_quote in order to extract common code, separate payment code
from sale model and controller code and ease reusing of payment code
in various addons.

Main features are

 * move account-related code from website_portal_sale to a new
   website_account module
 * move payment related to sale in a new sale_payment module
 * factorize JS code to handle payment through form and tokens
 * use generic sale payment code in website_sale and website_quote

Refer to individual commits for more details on the branch content.
This commit Closes #16956.
This commit is contained in:
Thibault Delavallée
2017-06-06 11:11:29 +02:00
44 changed files with 668 additions and 563 deletions
+1
View File
@@ -13,6 +13,7 @@
'views/payment_views.xml',
'views/account_payment_views.xml',
'views/payment_templates.xml',
'views/payment_assets.xml',
'views/res_partner_views.xml',
'security/ir.model.access.csv',
'security/payment_security.xml',
@@ -0,0 +1,81 @@
odoo.define('payment.transaction', function (require) {
"use strict";
var ajax = require('web.ajax');
$(document).ready(function () {
// When choosing an acquirer, display its Pay Now button
var $payment = $("#payment_method");
$payment.on("click", "input[name='acquirer'], a.btn_payment_token", function (ev) {
var ico_off = 'fa-circle-o';
var ico_on = 'fa-dot-circle-o';
var payment_id = $(ev.currentTarget).val() || $(this).data('acquirer');
var token = $(ev.currentTarget).data('token') || '';
$("div.o_payment_acquirer_button[data-id='"+payment_id+"']", $payment).attr('data-token', token);
$("div.js_payment a.list-group-item").removeClass("list-group-item-info");
$('span.js_radio').switchClass(ico_on, ico_off, 0);
if (token) {
$("div.o_payment_acquirer_button div.token_hide").hide();
$(ev.currentTarget).find('span.js_radio').switchClass(ico_off, ico_on, 0);
$(ev.currentTarget).parents('li').find('input').prop("checked", true);
$(ev.currentTarget).addClass("list-group-item-info");
}
else{
$("div.o_payment_acquirer_button div.token_hide").show();
}
$("div.o_payment_acquirer_button[data-id]", $payment).addClass("hidden");
$("div.o_payment_acquirer_button[data-id='"+payment_id+"']", $payment).removeClass("hidden");
})
.find("input[name='acquirer']:checked").click();
// When clicking on payment button: create the tx using json then continue to the acquirer
$payment.on("click", 'button[type="submit"], button[name="submit"]', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $form = $(ev.currentTarget).parents('form');
var acquirer = $(ev.currentTarget).parents('div.o_payment_acquirer_button').first();
var tx_data = acquirer.data();
if (! tx_data.id || ! tx_data.callUrl) {
return false;
}
var params = {"tx_type": tx_data.txType || acquirer.find('input[name="odoo_save_token"]').is(':checked') ? 'form_save': 'form'};
var acquirer_token = acquirer.attr('data-token'); // !=data
if (acquirer_token) {
params.token = acquirer_token;
}
if (tx_data.access_token) {
params.access_token = tx_data.access_token;
}
$form.off('submit');
ajax.jsonRpc(tx_data.callUrl, 'call', params).then(function (data) {
$(data).appendTo('body').submit();
});
return false;
});
$('div.oe_pay_token').on('click', 'a.js_btn_valid_tx', function() {
$('div.js_token_load').toggle();
var $form = $(this).parents('form');
ajax.jsonRpc($form.attr('action'), 'call', $.deparam($form.serialize())).then(function (data) {
if (data.url) {
window.location = data.url;
}
else {
$('div.js_token_load').toggle();
if (!data.success && data.error) {
$('div.oe_pay_token div.panel-body p').html(data.error + "<br/><br/>" + _('Retry ? '));
$('div.oe_pay_token div.panel-body').parents('div').removeClass('panel-info').addClass('panel-danger');
}
}
});
});
});
});
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Payment Tx Assets">
<xpath expr="." position="inside">
<script type="text/javascript" src="/payment/static/src/js/payment_transaction.js"></script>
</xpath>
</template>
</odoo>
@@ -36,13 +36,13 @@ odoo.define('payment_stripe.stripe', function(require) {
$(this).attr('disabled','disabled');
var $form = $(e.currentTarget).parents('form');
var acquirer_id = $(e.currentTarget).closest('div.oe_sale_acquirer_button,div.oe_quote_acquirer_button,div.o_website_payment_new_payment');
var acquirer_id = $(e.currentTarget).closest('div.o_payment_acquirer_button,div.oe_quote_acquirer_button,div.o_website_payment_new_payment');
acquirer_id = acquirer_id.data('id') || acquirer_id.data('acquirer_id');
if (! acquirer_id) {
return false;
}
var so_token = $("input[name='token']").val();
var access_token = $("input[name='token']").val();
var so_id = $("input[name='return_url']").val().match(/quote\/([0-9]+)/) || undefined;
if (so_id) {
so_id = parseInt(so_id[1]);
@@ -65,7 +65,7 @@ odoo.define('payment_stripe.stripe', function(require) {
} else {
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', {
so_id: so_id,
so_token: so_token
access_token: access_token
}, {'async': false}).then(function (data) {
$form.html(data);
handler.open({
+22
View File
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import uuid
from itertools import groupby
from datetime import datetime, timedelta
@@ -88,6 +90,9 @@ class SaleOrder(models.Model):
return '<p class=''oe_view_nocontent_create''">%s</p>' % (help)
return super(SaleOrder, self).get_empty_list_help(help)
def _get_default_access_token(self):
return str(uuid.uuid4())
@api.model
def _default_note(self):
return self.env.user.company_id.sale_note
@@ -110,6 +115,9 @@ class SaleOrder(models.Model):
name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New'))
origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.")
client_order_ref = fields.Char(string='Customer Reference', copy=False)
access_token = fields.Char(
'Security Token', copy=False,
default=_get_default_access_token)
state = fields.Selection([
('draft', 'Quotation'),
@@ -279,6 +287,20 @@ class SaleOrder(models.Model):
default['order_line'] = [(0, 0, line.copy_data()[0]) for line in self.order_line.filtered(lambda l: not l.is_downpayment)]
return super(SaleOrder, self).copy_data(default)
@api.model_cr_context
def _init_column(self, column_name):
""" Initialize the value of the given column for existing rows.
Overridden here because we skip generating unique access tokens
for potentially tons of existing sale orders, should they be needed,
they will be generated on the fly.
"""
if column_name != 'access_token':
super(SaleOrder, self)._init_column(column_name)
def _generate_access_token(self):
for order in self:
order.access_token = self._get_default_access_token()
@api.multi
def _prepare_invoice(self):
"""
+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
+23
View File
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
{
'name': 'Payment - Sale',
'category': 'Sales',
'summary': 'Link Sales and Payment',
'version': '1.0',
'description': """Link Sales and Payment
Provide tools for sale-related payment
* specific management of transactions like sale order confirmation
* tools method to handle transactions when allowing to pay / sell
* JS code to handle a payment form (eCommerce
* add payment-related fields on SO
""",
'depends': ['payment', 'sale'],
'data': [
'views/payment_views.xml',
],
'installable': True,
'auto_install': False,
}
+5
View File
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment
from . import sale_order
+185
View File
@@ -0,0 +1,185 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, fields, models, _
from odoo.tools import float_compare
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
# link with the sales order
# YTI FIXME: The auto_join seems useless
sale_order_id = fields.Many2one('sale.order', string='Sales Order', auto_join=True)
# --------------------------------------------------
# Sale management
# --------------------------------------------------
@api.model
def form_feedback(self, data, acquirer_name):
""" Override to confirm the sales order, if defined, and if the transaction
is done. """
tx = None
res = super(PaymentTransaction, self).form_feedback(data, acquirer_name)
# fetch the tx
tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
if hasattr(self, tx_find_method_name):
tx = getattr(self, tx_find_method_name)(data)
_logger.info('<%s> transaction processed: tx ref:%s, tx amount: %s', acquirer_name, tx.reference if tx else 'n/a', tx.amount if tx else 'n/a')
if tx:
# Auto-confirm SO if necessary
tx._confirm_so()
return res
def _confirm_so(self):
for tx in self:
# check tx state, confirm the potential SO
if not tx.sale_order_id or tx.sale_order_id.state not in ['draft', 'sent']:
# _logger.warning('<%s> transaction incorrect sale order %s (ID %s, state %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id, tx.sale_order_id.state)
continue
if not float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0:
_logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id)
continue
try:
if tx.state == 'authorized' and tx.acquirer_id.capture_manually:
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.with_context(send_email=True).action_confirm()
if tx.state == 'done':
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.with_context(send_email=True).action_confirm()
tx._generate_and_pay_invoice()
elif tx.state not in ['cancel', 'error'] and tx.sale_order_id.state == 'draft':
_logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.force_quotation_send()
else:
_logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id)
except Exception:
_logger.exception('Fail to confirm the order or send the confirmation email%s', tx and ' for the transaction %s' % tx.reference or '')
def _generate_and_pay_invoice(self):
self.sale_order_id._force_lines_to_invoice_policy_order()
# force company to ensure journals/accounts etc. are correct
# company_id needed for default_get on account.journal
# force_company needed for company_dependent fields
ctx_company = {'company_id': self.sale_order_id.company_id.id,
'force_company': self.sale_order_id.company_id.id}
created_invoice = self.sale_order_id.with_context(**ctx_company).action_invoice_create()
created_invoice = self.env['account.invoice'].browse(created_invoice).with_context(**ctx_company)
if created_invoice:
_logger.info('<%s> transaction completed, auto-generated invoice %s (ID %s) for %s (ID %s)',
self.acquirer_id.provider, created_invoice.name, created_invoice.id, self.sale_order_id.name, self.sale_order_id.id)
created_invoice.action_invoice_open()
if not self.acquirer_id.journal_id:
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if not default_journal:
_logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)',
self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
self.acquirer_id.journal_id = default_journal
created_invoice.pay_and_reconcile(self.acquirer_id.journal_id, pay_amount=created_invoice.amount_total)
if created_invoice.payment_ids:
created_invoice.payment_ids[0].payment_transaction_id = self
else:
_logger.warning('<%s> transaction completed, could not auto-generate invoice for %s (ID %s)',
self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id)
# --------------------------------------------------
# Tools for payment
# --------------------------------------------------
def confirm_sale_token(self):
""" Confirm a transaction token and call SO confirmation if it is a success.
:return: True if success; error string otherwise """
self.ensure_one()
if self.payment_token_id and self.partner_id == self.sale_order_id.partner_id:
try:
s2s_result = self.s2s_do_transaction()
except Exception as e:
_logger.warning(_("Payment transaction (%s) failed : <%s>") % (self.id, str(e)))
return _("Payment transaction failed (Contact Administrator)")
valid_state = 'authorized' if self.acquirer_id.capture_manually else 'done'
if not s2s_result or self.state != valid_state:
return _("Payment transaction failed (%s)" % self.state_message)
try:
# Auto-confirm SO if necessary
self._confirm_so()
return True
except Exception as e:
_logger.warning(_("Payment transaction (%s) failed : <%s>") % (self.id, str(e)))
return _("Payment transaction / SO Confirmation failed (Contact Administrator)")
return _('Tx missmatch')
def check_or_create_sale_tx(self, order, acquirer, payment_token=None, tx_type='form', add_tx_values=None, reset_draft=True):
tx = self
# incorrect state or unexisting tx
if not self or self.state in ['error', 'cancel']:
tx = False
# unmatching
if (self and acquirer and self.acquirer_id != acquirer) or (self and self.sale_order_id != order):
tx = False
# new or distinct token
if payment_token and tx.payment_token_id and payment_token != self.payment_token_id:
tx = False
# still draft tx, no more info -> rewrite on tx or create a new one depending on parameter
if tx and tx.state == 'draft':
if reset_draft:
tx.write(dict(
self.on_change_partner_id(order.partner_id.id).get('value', {}),
amount=order.amount_total,
type=tx_type)
)
else:
tx = False
if not tx:
tx_values = {
'acquirer_id': acquirer.id,
'type': tx_type,
'amount': order.amount_total,
'currency_id': order.pricelist_id.currency_id.id,
'partner_id': order.partner_id.id,
'partner_country_id': order.partner_id.country_id.id,
'reference': self.get_next_reference(order.name),
'sale_order_id': order.id,
}
if add_tx_values:
tx_values.update(add_tx_values)
if payment_token and payment_token.sudo().partner_id == order.partner_id:
tx_values['payment_token_id'] = payment_token.id
tx = self.create(tx_values)
# update quotation
order.write({
'payment_tx_id': tx.id,
})
return tx
def render_sale_button(self, order, return_url, submit_txt=None, render_values=None):
values = {
'return_url': return_url,
'partner_id': order.partner_shipping_id.id or order.partner_invoice_id.id,
'billing_partner_id': order.partner_invoice_id.id,
}
if render_values:
values.update(render_values)
return self.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=submit_txt or _('Pay Now')).sudo().render(
self.reference,
order.amount_total,
order.pricelist_id.currency_id.id,
values=values,
)
+11
View File
@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
payment_tx_id = fields.Many2one('payment.transaction', string='Last Transaction', copy=False)
payment_acquirer_id = fields.Many2one('payment.acquirer', string='Payment Acquirer', related='payment_tx_id.acquirer_id', store=True)
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_transaction_action_pending" model="ir.actions.act_window">
<field name="name">Pending Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
<field name='domain'>[('sale_order_id.team_id', '=', active_id), ('state', '=', 'pending')]</field>
</record>
<record id="payment_transaction_action_authorized" model="ir.actions.act_window">
<field name="name">Authorized Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
<field name='domain'>[('sale_order_id.team_id', '=', active_id), ('state', '=', 'authorized')]</field>
</record>
</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 controllers
from . import models
+25
View File
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Account Customer Portal',
'category': 'Website',
'summary': 'Add your accounting document in the frontend portal',
'version': '1.0',
'description': """
Add your accounting document in the frontend portal. Give customers the list and the state of their invoices.
""",
'depends': [
'account',
'website_portal',
'website_payment',
],
'data': [
'views/account_views.xml',
'views/account_templates.xml',
'security/ir.model.access.csv',
'security/account.xml',
],
'demo': [],
'installable': True,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import portal_account
@@ -0,0 +1,106 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, _
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.addons.website_portal.controllers.main import website_account
class PortalAccount(website_account):
def _prepare_portal_layout_values(self):
values = super(PortalAccount, self)._prepare_portal_layout_values()
partner = request.env.user.partner_id
invoice_count = request.env['account.invoice'].search_count([
('type', 'in', ['out_invoice', 'out_refund']),
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', 'in', ['open', 'paid', 'cancel'])
])
values['invoice_count'] = invoice_count
return values
@http.route(['/my/invoices', '/my/invoices/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_invoices(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
partner = request.env.user.partner_id
AccountInvoice = request.env['account.invoice']
domain = [
('type', 'in', ['out_invoice', 'out_refund']),
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', 'in', ['open', 'paid', 'cancelled'])
]
searchbar_sortings = {
'date': {'label': _('Invoice Date'), 'order': 'date_invoice desc'},
'duedate': {'label': _('Due Date'), 'order': 'date_due desc'},
'name': {'label': _('Reference'), 'order': 'name desc'},
'state': {'label': _('Status'), 'order': 'state'},
}
# default sort by order
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
archive_groups = self._get_archive_groups('account.invoice', domain)
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# count for pager
invoice_count = AccountInvoice.search_count(domain)
# pager
pager = request.website.pager(
url="/my/invoices",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
total=invoice_count,
page=page,
step=self._items_per_page
)
# content according to pager and archive selected
invoices = AccountInvoice.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
values.update({
'date': date_begin,
'invoices': invoices,
'page_name': 'invoice',
'pager': pager,
'archive_groups': archive_groups,
'default_url': '/my/invoices',
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
})
return request.render("website_account.portal_my_invoices", values)
@http.route(['/my/invoices/<int:invoice_id>'], type='http', auth="user", website=True)
def portal_my_invoices_report(self, invoice_id, **kw):
invoice = request.env['account.invoice'].browse(invoice_id)
try:
invoice.check_access_rights('read')
invoice.check_access_rule('read')
except AccessError:
return request.render("website.403")
# print report as sudo, since it require access to taxes, payment term, ... and portal
# does not have those access rights.
pdf = request.env.ref('account.account_invoices').sudo().render_qweb_pdf([invoice_id])[0]
pdfhttpheaders = [
('Content-Type', 'application/pdf'),
('Content-Length', len(pdf)),
]
return request.make_response(pdf, headers=pdfhttpheaders)
def details_form_validate(self, data):
error, error_message = super(website_account, self).details_form_validate(data)
# prevent VAT/name change if invoices exist
partner = request.env['res.users'].browse(request.uid).partner_id
invoices = request.env['account.invoice'].sudo().search_count([('partner_id', '=', partner.id), ('state', 'not in', ['draft', 'cancel'])])
if invoices:
if 'vat' in partner and (data['vat'] or False) != (partner.vat or False):
error['vat'] = 'error'
error_message.append(_('Changing VAT number is not allowed once invoices have been issued for your account. Please contact us directly for this operation.'))
if 'name' in data and (data['name'] or False) != (partner.name or False):
error['name'] = 'error'
error_message.append(_('Changing your name is not allowed once invoices have been issued for your account. Please contact us directly for this operation.'))
return error, error_message
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_invoice
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="portal_account_invoice_user_rule" model="ir.rule">
<field name="name">Portal Personal Account Invoices</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="domain_force">[('message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="portal_account_invoice_line_rule" model="ir.rule">
<field name="name">Portal Invoice Lines</field>
<field name="model_id" ref="account.model_account_invoice_line"/>
<field name="domain_force">[('invoice_id.message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</data>
</odoo>
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_account_invoice,account.invoice,account.model_account_invoice,base.group_portal,1,0,0,0
access_account_invoice_line,account.invoice.line,account.model_account_invoice_line,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_account_invoice account.invoice account.model_account_invoice base.group_portal 1 0 0 0
3 access_account_invoice_line account.invoice.line account.model_account_invoice_line base.group_portal 1 0 0 0
@@ -0,0 +1,74 @@
<odoo>
<template id="portal_my_home_menu_invoice" name="Portal layout : invoice menu entries" inherit_id="website_portal.portal_layout" priority="30">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="invoice_count" t-att-class="page_name == 'invoice' and 'active'">
<a href="/my/invoices">Invoices</a>
</li>
</xpath>
</template>
<template id="portal_my_home_invoice" name="Portal My Home : invoice entries" inherit_id="website_portal.portal_my_home" priority="30">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="invoice_count" class="page-header">
<a href="/my/invoices">Your Invoices and Payments
<small class="ml8">
<span class='badge'><t t-esc="invoice_count"/></span>
</small>
</a>
</h3>
</xpath>
</template>
<template id="portal_my_invoices" name="My Invoices and Payments">
<t t-call="website_portal.portal_layout">
<div class="page-header">
<span class="h3">Your Invoices</span>
<t t-call="website_portal.portal_searchbar"/>
</div>
<t t-if="not invoices">
<p>There are currently no invoices and payments for your account.</p>
</t>
<t t-if="invoices">
<div class="table-responsive"><table class="table table-hover o_my_status_table">
<thead>
<tr class="active">
<th>Invoice #</th>
<th>Invoice Date</th>
<th class='hidden-xs'>Due Date</th>
<th></th>
<th></th>
<th>Amount Due</th>
</tr>
</thead>
<t t-foreach="invoices" t-as="invoice">
<tr>
<td>
<a t-att-href="'/my/invoices/'+str(invoice.id)" t-att-title="invoice.number"><t t-esc="invoice.number"/></a>
</td>
<td><span t-field="invoice.date_invoice"/></td>
<td class='hidden-xs'><span t-field="invoice.date_due"/></td>
<td>
<t t-if="invoice.state == 'open'">
<span class="label label-info"><i class="fa fa-fw fa-clock-o"></i><span class="hidden-xs"> Waiting for Payment</span></span>
</t>
<t t-if="invoice.state == 'paid'">
<span class="label label-default"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Paid</span></span>
</t>
<t t-if="invoice.state == 'cancel'">
<span class="label label-default"><i class="fa fa-fw fa-remove"></i><span class="hidden-xs"> Cancelled</span></span>
</t>
</td>
<td>
<a t-if="invoice.state == 'open'" t-attf-href="/website_payment/pay?reference=#{invoice.number}&amp;amount=#{invoice.residual}&amp;currency_id=#{invoice.currency_id.id}&amp;country_id=#{invoice.partner_id.country_id.id}" alt="Pay Now" class="btn btn-xs btn-primary"><i class="fa fa-arrow-circle-right"/><span class='hidden-xs'> Pay Now</span></a>
</td>
<td><span t-field="invoice.residual" t-options='{"widget": "monetary", "display_currency": invoice.currency_id}'/></td>
</tr>
</t>
</table></div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</t>
</t>
</template>
</odoo>
@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<odoo>
<record id="view_account_invoice_filter_share" model="ir.ui.view">
<field name="name">account.invoice.select.share</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="user_id"]' position="attributes">
<attribute name="domain">[('share','=', False)]</attribute>
</xpath>
</field>
</record>
</odoo>
@@ -86,7 +86,7 @@ tour.register('event_buy_tickets', {
{
content: "Pay",
extra_trigger: '#payment_method label:contains(Wire Transfer) input:checked',
trigger: '.oe_sale_acquirer_button .btn[type="submit"]:visible',
trigger: '.o_payment_acquirer_button .btn[type="submit"]:visible',
},
{
content: "Last step",
@@ -1,2 +1 @@
from . import payment
from . import website
-7
View File
@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class PaymentAcquirer(models.Model):
_name = 'payment.acquirer'
_inherit = ['payment.acquirer','website.published.mixin']
+4 -3
View File
@@ -4,20 +4,21 @@
{
'name': 'Website Portal for Sales',
'category': 'Website',
'summary': 'Add your sales document in the frontend portal (sales order, quotations, invoices)',
'summary': 'Add your sales document in the frontend portal (sales order, quotations)',
'version': '1.0',
'description': """
Add your sales document in the frontend portal. Your customers will be able to connect to their portal to see the list (and the state) of their invoices (pdf report), sales orders and quotations (web pages).
Add your sales document in the frontend portal. Your customers will be able to connect to their portal to see the list (and the state) of their sales orders and quotations (web pages).
""",
'depends': [
'sale',
'sale_payment',
'website_portal',
'website_payment',
],
'data': [
'views/crm_team_views.xml',
'views/website_portal_sale_templates.xml',
'views/sale_order_views.xml',
'views/website_portal_sale_templates.xml',
'security/ir.model.access.csv',
'security/sale_order_security.xml',
],
@@ -15,7 +15,6 @@ class website_account(website_account):
partner = request.env.user.partner_id
SaleOrder = request.env['sale.order']
Invoice = request.env['account.invoice']
quotation_count = SaleOrder.search_count([
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', 'in', ['sent', 'cancel'])
@@ -24,16 +23,10 @@ class website_account(website_account):
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', 'in', ['sale', 'done'])
])
invoice_count = Invoice.search_count([
('type', 'in', ['out_invoice', 'out_refund']),
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', 'in', ['open', 'paid', 'cancel'])
])
values.update({
'quotation_count': quotation_count,
'order_count': order_count,
'invoice_count': invoice_count,
})
return values
@@ -160,89 +153,3 @@ class website_account(website_account):
}
values.update(get_records_pager(history, order))
return request.render("website_portal_sale.orders_followup", values)
#
# Invoices
#
@http.route(['/my/invoices', '/my/invoices/page/<int:page>'], type='http', auth="user", website=True)
def portal_my_invoices(self, page=1, date_begin=None, date_end=None, sortby=None, **kw):
values = self._prepare_portal_layout_values()
partner = request.env.user.partner_id
AccountInvoice = request.env['account.invoice']
domain = [
('type', 'in', ['out_invoice', 'out_refund']),
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
('state', 'in', ['open', 'paid', 'cancelled'])
]
searchbar_sortings = {
'date': {'label': _('Invoice Date'), 'order': 'date_invoice desc'},
'duedate': {'label': _('Due Date'), 'order': 'date_due desc'},
'name': {'label': _('Reference'), 'order': 'name desc'},
'state': {'label': _('Status'), 'order': 'state'},
}
# default sort by order
if not sortby:
sortby = 'date'
order = searchbar_sortings[sortby]['order']
archive_groups = self._get_archive_groups('account.invoice', domain)
if date_begin and date_end:
domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)]
# count for pager
invoice_count = AccountInvoice.search_count(domain)
# pager
pager = request.website.pager(
url="/my/invoices",
url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby},
total=invoice_count,
page=page,
step=self._items_per_page
)
# content according to pager and archive selected
invoices = AccountInvoice.search(domain, order=order, limit=self._items_per_page, offset=pager['offset'])
values.update({
'date': date_begin,
'invoices': invoices,
'page_name': 'invoice',
'pager': pager,
'archive_groups': archive_groups,
'default_url': '/my/invoices',
'searchbar_sortings': searchbar_sortings,
'sortby': sortby,
})
return request.render("website_portal_sale.portal_my_invoices", values)
@http.route(['/my/invoices/<int:invoice_id>'], type='http', auth="user", website=True)
def portal_my_invoices_report(self, invoice_id, **kw):
invoice = request.env['account.invoice'].browse(invoice_id)
try:
invoice.check_access_rights('read')
invoice.check_access_rule('read')
except AccessError:
return request.render("website.403")
# print report as sudo, since it require access to taxes, payment term, ... and portal
# does not have those access rights.
pdf = request.env.ref('account.account_invoices').sudo().render_qweb_pdf([invoice_id])[0]
pdfhttpheaders = [
('Content-Type', 'application/pdf'),
('Content-Length', len(pdf)),
]
return request.make_response(pdf, headers=pdfhttpheaders)
def details_form_validate(self, data):
error, error_message = super(website_account, self).details_form_validate(data)
# prevent VAT/name change if invoices exist
partner = request.env['res.users'].browse(request.uid).partner_id
invoices = request.env['account.invoice'].sudo().search_count([('partner_id', '=', partner.id), ('state', 'not in', ['draft', 'cancel'])])
if invoices:
if 'vat' in partner and (data['vat'] or False) != (partner.vat or False):
error['vat'] = 'error'
error_message.append(_('Changing VAT number is not allowed once invoices have been issued for your account. Please contact us directly for this operation.'))
if 'name' in data and (data['name'] or False) != (partner.name or False):
error['name'] = 'error'
error_message.append(_('Changing your name is not allowed once invoices have been issued for your account. Please contact us directly for this operation.'))
return error, error_message
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_invoice
from . import crm_team
from . import sale_order
from . import payment
@@ -1,89 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from odoo import api, fields, models
from odoo.tools import float_compare
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
# link with the sales order
# YTI FIXME: The auto_join seems useless
sale_order_id = fields.Many2one('sale.order', string='Sales Order', auto_join=True)
def _generate_and_pay_invoice(self, tx, acquirer_name):
tx.sale_order_id._force_lines_to_invoice_policy_order()
# force company to ensure journals/accounts etc. are correct
# company_id needed for default_get on account.journal
# force_company needed for company_dependent fields
ctx_company = {'company_id': tx.sale_order_id.company_id.id,
'force_company': tx.sale_order_id.company_id.id}
created_invoice = tx.sale_order_id.with_context(**ctx_company).action_invoice_create()
created_invoice = self.env['account.invoice'].browse(created_invoice).with_context(**ctx_company)
if created_invoice:
_logger.info('<%s> transaction completed, auto-generated invoice %s (ID %s) for %s (ID %s)',
acquirer_name, created_invoice.name, created_invoice.id, tx.sale_order_id.name, tx.sale_order_id.id)
created_invoice.action_invoice_open()
if not tx.acquirer_id.journal_id:
default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1)
if not default_journal:
_logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)',
acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.acquirer_id.journal_id = default_journal
created_invoice.pay_and_reconcile(tx.acquirer_id.journal_id, pay_amount=created_invoice.amount_total)
if created_invoice.payment_ids:
created_invoice.payment_ids[0].payment_transaction_id = tx
else:
_logger.warning('<%s> transaction completed, could not auto-generate invoice for %s (ID %s)',
acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
@api.model
def form_feedback(self, data, acquirer_name):
""" Override to confirm the sales order, if defined, and if the transaction
is done. """
tx = None
res = super(PaymentTransaction, self).form_feedback(data, acquirer_name)
# fetch the tx
tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name
if hasattr(self, tx_find_method_name):
tx = getattr(self, tx_find_method_name)(data)
_logger.info('<%s> transaction processed: tx ref:%s, tx amount: %s', acquirer_name, tx.reference if tx else 'n/a', tx.amount if tx else 'n/a')
if tx:
# Auto-confirm SO if necessary
tx._confirm_so(acquirer_name=acquirer_name)
return res
def _confirm_so(self, acquirer_name=False):
for tx in self:
try:
# check tx state, confirm the potential SO
if tx.sale_order_id and tx.sale_order_id.state in ['draft', 'sent']:
# verify SO/TX match, excluding tx.fees which are currently not included in SO
amount_matches = float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0
if amount_matches:
if not acquirer_name:
acquirer_name = tx.sale_order_id.payment_acquirer_id.provider or 'unknown'
if tx.state == 'authorized' and tx.acquirer_id.capture_manually:
_logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.with_context(send_email=True).action_confirm()
if tx.state == 'done':
_logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.with_context(send_email=True).action_confirm()
self._generate_and_pay_invoice(tx, acquirer_name)
elif tx.state not in ['cancel', 'error'] and tx.sale_order_id.state == 'draft':
_logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
tx.sale_order_id.force_quotation_send()
else:
_logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id)
except Exception:
_logger.exception('Fail to confirm the order or send the confirmation email%s', tx and ' for the transaction %s' % tx.reference or '')
@@ -1,5 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_sale_order,sale.order,sale.model_sale_order,base.group_portal,1,0,0,0
access_sale_order_line,sale.order.line,sale.model_sale_order_line,base.group_portal,1,0,0,0
access_account_invoice,account.invoice,account.model_account_invoice,base.group_portal,1,0,0,0
access_account_invoice_line,account.invoice.line,account.model_account_invoice_line,base.group_portal,1,0,0,0
access_sale_order_line,sale.order.line,sale.model_sale_order_line,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_sale_order sale.order sale.model_sale_order base.group_portal 1 0 0 0
3 access_sale_order_line sale.order.line sale.model_sale_order_line base.group_portal 1 0 0 0
access_account_invoice account.invoice account.model_account_invoice base.group_portal 1 0 0 0
access_account_invoice_line account.invoice.line account.model_account_invoice_line base.group_portal 1 0 0 0
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sale Portal Access Rules -->
<record id="portal_sale_order_user_rule" model="ir.rule">
<field name="name">Portal Personal Quotations/Sales Orders</field>
@@ -20,21 +19,5 @@
<field name="domain_force">[('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="portal_account_invoice_user_rule" model="ir.rule">
<field name="name">Portal Personal Account Invoices</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="domain_force">[('message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
<record id="portal_account_invoice_line_rule" model="ir.rule">
<field name="name">Portal Invoice Lines</field>
<field name="model_id" ref="account.model_account_invoice_line"/>
<field name="domain_force">[('invoice_id.message_partner_ids','child_of',[user.commercial_partner_id.id])]</field>
<field name="groups" eval="[(4, ref('base.group_portal'))]"/>
</record>
</data>
</odoo>
@@ -8,7 +8,3 @@
.orders_label_text_align {
vertical-align: 15%;
}
.cell_overflow {
overflow: hidden;
}
@@ -1,22 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="payment_transaction_action_pending" model="ir.actions.act_window">
<field name="name">Pending Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
<field name='domain'>[('sale_order_id.team_id', '=', active_id), ('state', '=', 'pending')]</field>
</record>
<record id="payment_transaction_action_authorized" model="ir.actions.act_window">
<field name="name">Authorized Payment Transactions</field>
<field name="res_model">payment.transaction</field>
<field name='view_type'>form</field>
<field name='view_mode'>tree,form</field>
<field name='domain'>[('sale_order_id.team_id', '=', active_id), ('state', '=', 'authorized')]</field>
</record>
<record id="crm_team_salesteams_view_kanban_inherit_website_portal_sale" model="ir.ui.view">
<field name="name">crm.team.kanban</field>
<field name="model">crm.team</field>
@@ -27,7 +11,7 @@
<div class="row" t-if="record.pending_payment_transactions_count.raw_value">
<div class="col-xs-8">
<div>
<a name="%(payment_transaction_action_pending)d" type="action">
<a name="%(sale_payment.payment_transaction_action_pending)d" type="action">
<field name="pending_payment_transactions_count"/> Awaiting Payment<t t-if="record.pending_payment_transactions_count.raw_value != 1">s</t>
</a>
</div>
@@ -38,7 +22,7 @@
</div>
<div class="row" t-if="record.authorized_payment_transactions_count.raw_value">
<div class="col-xs-8">
<a name="%(payment_transaction_action_authorized)d" type="action">
<a name="%(sale_payment.payment_transaction_action_authorized)d" type="action">
<field name="authorized_payment_transactions_count"/> Payment<t t-if="record.authorized_payment_transactions_count.raw_value != 1">s</t> to Capture
</a>
</div>
@@ -1,16 +1,5 @@
<?xml version="1.0"?>
<odoo>
<record id="view_account_invoice_filter_share" model="ir.ui.view">
<field name="name">account.invoice.select.share</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.view_account_invoice_filter"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="user_id"]' position="attributes">
<attribute name="domain">[('share','=', False)]</attribute>
</xpath>
</field>
</record>
<record id="sale_order_view_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
@@ -17,14 +17,6 @@
</xpath>
</template>
<template id="portal_my_home_menu_invoice" name="Portal layout : invoice menu entries" inherit_id="website_portal.portal_layout" priority="30">
<xpath expr="//ul[contains(@class,'o_portal_submenu')]" position="inside">
<li t-if="invoice_count" t-att-class="page_name == 'invoice' and 'active'">
<a href="/my/invoices">Invoices</a>
</li>
</xpath>
</template>
<template id="portal_my_home_sale" name="Portal My Home : sales entries" inherit_id="website_portal.portal_my_home" priority="20">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="quotation_count" class="page-header">
@@ -49,18 +41,6 @@
</xpath>
</template>
<template id="portal_my_home_invoice" name="Portal My Home : invoice entries" inherit_id="website_portal.portal_my_home" priority="30">
<xpath expr="//div[contains(@class,'o_my_home_content')]" position="inside">
<h3 t-if="invoice_count" class="page-header">
<a href="/my/invoices">Your Invoices and Payments
<small class="ml8">
<span class='badge'><t t-esc="invoice_count"/></span>
</small>
</a>
</h3>
</xpath>
</template>
<template id="portal_my_quotations" name="My Quotations">
<t t-call="website_portal.portal_layout">
<div class="page-header">
@@ -161,59 +141,6 @@
</t>
</template>
<template id="portal_my_invoices" name="My Invoices and Payments">
<t t-call="website_portal.portal_layout">
<div class="page-header">
<span class="h3">Your Invoices</span>
<t t-call="website_portal.portal_searchbar"/>
</div>
<t t-if="not invoices">
<p>There are currently no invoices and payments for your account.</p>
</t>
<t t-if="invoices">
<div class="table-responsive"><table class="table table-hover o_my_status_table">
<thead>
<tr class="active">
<th>Invoice #</th>
<th>Invoice Date</th>
<th class='hidden-xs'>Due Date</th>
<th></th>
<th></th>
<th>Amount Due</th>
</tr>
</thead>
<t t-foreach="invoices" t-as="invoice">
<tr>
<td class= "cell_overflow">
<a t-att-href="'/my/invoices/'+str(invoice.id)" t-att-title="invoice.number"><t t-esc="invoice.number"/></a>
</td>
<td><span t-field="invoice.date_invoice"/></td>
<td class='hidden-xs'><span t-field="invoice.date_due"/></td>
<td>
<t t-if="invoice.state == 'open'">
<span class="label label-info"><i class="fa fa-fw fa-clock-o"></i><span class="hidden-xs"> Waiting for Payment</span></span>
</t>
<t t-if="invoice.state == 'paid'">
<span class="label label-default"><i class="fa fa-fw fa-check"></i><span class="hidden-xs"> Paid</span></span>
</t>
<t t-if="invoice.state == 'cancel'">
<span class="label label-default"><i class="fa fa-fw fa-remove"></i><span class="hidden-xs"> Cancelled</span></span>
</t>
</td>
<td>
<a t-if="invoice.state == 'open'" t-attf-href="/website_payment/pay?reference=#{invoice.number}&amp;amount=#{invoice.residual}&amp;currency_id=#{invoice.currency_id.id}&amp;country_id=#{invoice.partner_id.country_id.id}" alt="Pay Now" class="btn btn-xs btn-primary"><i class="fa fa-arrow-circle-right"/><span class='hidden-xs'> Pay Now</span></a>
</td>
<td><span t-field="invoice.residual" t-options='{"widget": "monetary", "display_currency": invoice.currency_id}'/></td>
</tr>
</t>
</table></div>
<div t-if="pager" class="o_portal_pager text-center">
<t t-call="website.pager"/>
</div>
</t>
</t>
</template>
<template id="orders_followup" name="Sales Order">
<t t-call="website.layout">
<div id="wrap">
+18 -50
View File
@@ -158,11 +158,7 @@ class sale_quote(http.Controller):
# note dbo: website_sale code
@http.route(['/quote/<int:order_id>/transaction/<int:acquirer_id>'], type='json', auth="public", website=True)
def payment_transaction(self, acquirer_id, order_id):
return self.payment_transaction_token(acquirer_id, order_id, None)
@http.route(['/quote/<int:order_id>/transaction/<int:acquirer_id>/<token>'], type='json', auth="public", website=True)
def payment_transaction_token(self, acquirer_id, order_id, token):
def payment_transaction_token(self, acquirer_id, order_id, access_token=None, tx_type=None):
""" Json method that creates a payment.transaction, used to create a
transaction when the user clicks on 'pay now' button. After having
created the transaction, the event continues and the user is redirected
@@ -171,52 +167,24 @@ class sale_quote(http.Controller):
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
"""
PaymentTransaction = request.env['payment.transaction'].sudo()
Order = request.env['sale.order'].sudo().browse(order_id)
if not Order or not Order.order_line or acquirer_id is None:
order = request.env['sale.order'].sudo().browse(order_id)
if not order or not order.order_line or acquirer_id is None:
return request.redirect("/quote/%s" % order_id)
# find an already existing transaction
Transaction = PaymentTransaction.search([('reference', '=', Order.name)])
if Transaction:
if Transaction.sale_order_id != Order or Transaction.state in ['error', 'cancel'] or Transaction.acquirer_id.id != acquirer_id:
Transaction = False
elif Transaction.state == 'draft':
Transaction.write({
'amount': Order.amount_total,
})
if not Transaction:
Transaction = PaymentTransaction.create({
'acquirer_id': acquirer_id,
'type': Order._get_payment_type(),
'amount': Order.amount_total,
'currency_id': Order.pricelist_id.currency_id.id,
'partner_id': Order.partner_id.id,
'reference': PaymentTransaction.get_next_reference(Order.name),
'sale_order_id': Order.id,
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', Order._name)], limit=1).id,
'callback_res_id': Order.id,
'callback_method': '_confirm_online_quote',
})
request.session['quote_%s_transaction_id' % Order.id] = Transaction.id
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
token = request.env['payment.token'].sudo() # currently no support of payment tokens
tx = request.env['payment.transaction'].sudo().search([('reference', '=', order.name)], limit=1)
tx_type = order._get_payment_type()
tx = tx.check_or_create_sale_tx(order, acquirer, payment_token=token, tx_type=tx_type, add_tx_values={
'callback_model_id': request.env['ir.model'].sudo().search([('model', '=', order._name)], limit=1).id,
'callback_res_id': order.id,
'callback_method': '_confirm_online_quote',
})
request.session['quote_%s_transaction_id' % order.id] = tx.id
# update quotation
Order.write({
'payment_acquirer_id': acquirer_id,
'payment_tx_id': Transaction.id
})
return Transaction.acquirer_id.with_context(
submit_class='btn btn-primary',
submit_txt=_('Pay & Confirm')).render(
Transaction.reference,
Order.amount_total,
Order.pricelist_id.currency_id.id,
values={
'return_url': '/quote/%s/%s' % (order_id, token) if token else '/quote/%s' % order_id,
'type': Order._get_payment_type(),
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
'partner_id': Order.partner_shipping_id.id or Order.partner_invoice_id.id,
'billing_partner_id': Order.partner_invoice_id.id,
})
return tx.render_sale_button(order, '/quote/%s/%s' % (order_id, token) if token else '/quote/%s' % order_id,
submit_txt=_('Pay & Confirm'), render_values={
'type': order._get_payment_type(),
'alias_usage': _('If we store your payment information on our server, subscription payments will be made automatically.'),
})
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import uuid
from datetime import datetime, timedelta
from odoo import api, fields, models, _
@@ -52,9 +51,6 @@ class SaleOrder(models.Model):
if so.state not in ['sale', 'done']:
so.website_url = '/quote/%s' % (so.id)
access_token = fields.Char(
'Security Token', copy=False, default=lambda self: str(uuid.uuid4()),
required=True)
template_id = fields.Many2one(
'sale.quote.template', 'Quotation Template',
readonly=True,
@@ -4,7 +4,6 @@ odoo.define('website_quote.website_quote', function (require) {
var ajax = require('web.ajax');
var config = require('web.config');
var Widget = require('web.Widget');
var website = require('website.website');
if(!$('.o_website_quote').length) {
return $.Deferred().reject("DOM doesn't contain '.o_website_quote'");
@@ -200,44 +199,3 @@ if(!$('.o_website_quote').length) {
},
});
});
odoo.define('website_quote.payment_method', function (require) {
'use strict';
require('website.website');
var ajax = require('web.ajax');
if(!$('#payment_method').length) {
return $.Deferred().reject("DOM doesn't contain '#payment_method'");
}
// dbo note: website_sale code for payment
// if we standardize payment somehow, this should disappear
// When choosing an acquirer, display its Pay Now button
var $payment = $("#payment_method");
$payment.on("click", "input[name='acquirer']", function (ev) {
var payment_id = $(ev.currentTarget).val();
$("div.oe_quote_acquirer_button[data-id]", $payment).addClass("hidden");
$("div.oe_quote_acquirer_button[data-id='"+payment_id+"']", $payment).removeClass("hidden");
})
.find("input[name='acquirer']:checked").click();
// When clicking on payment button: create the tx using json then continue to the acquirer
$('.oe_quote_acquirer_button').on("click", 'button[type="submit"],button[name="submit"]', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $form = $(ev.currentTarget).parents('form');
var acquirer_id = $(ev.currentTarget).parents('.oe_quote_acquirer_button').first().data('id');
if (! acquirer_id) {
return false;
}
var href = $(location).attr("href");
var order_id = href.match(/quote\/([0-9]+)/)[1];
var token = href.match(/quote\/[0-9]+\/([^\/?]*)/);
token = token ? token[1] : '';
ajax.jsonRpc('/quote/' + order_id +'/transaction/' + acquirer_id + (token ? '/' + token : ''), 'call', {}).then(function (data) {
$form.html(data);
$form.submit();
});
});
});
@@ -366,9 +366,13 @@
</div>
<div class="col-md-6">
<t t-foreach="acquirers or []" t-as="acquirer">
<div t-att-data-id="acquirer.id" class="oe_quote_acquirer_button hidden pull-right">
<div t-raw="buttons.get(acquirer.id)"/>
<div t-field="acquirer.pre_msg"/>
<div class="o_payment_acquirer_button hidden pull-right"
t-att-data-id="acquirer.id"
t-att-data-access-token="token or ''"
t-att-data-tx-type="form"
t-att-data-url="'/quote/%s/transaction/%s' % (quotation.id, acquirer.id)">
<div t-raw="buttons.get(acquirer.id)"/>
<div t-field="acquirer.pre_msg"/>
</div>
</t>
<button type="button" class="btn btn-link" data-dismiss="modal" style="padding: 0">Cancel</button>
+1 -1
View File
@@ -10,7 +10,7 @@ Odoo E-Commerce
==================
""",
'depends': ['website', 'sale', 'payment', 'website_payment', 'website_portal_sale', 'website_mail', 'website_form', 'rating'],
'depends': ['website', 'sale', 'payment', 'website_payment', 'website_portal_sale', 'website_account', 'website_mail', 'website_form', 'rating'],
'data': [
'security/ir.model.access.csv',
'security/website_sale.xml',
+16 -62
View File
@@ -762,23 +762,12 @@ class WebsiteSale(http.Controller):
@http.route(['/shop/payment/transaction_token/confirm'], type='json', auth="public", website=True)
def payment_transaction_token_confirm(self, tx, **kwargs):
tx = request.env['payment.transaction'].sudo().browse(int(tx))
if (tx and request.website.sale_get_transaction() and
tx.id == request.website.sale_get_transaction().id and
tx.payment_token_id and
tx.partner_id == tx.sale_order_id.partner_id):
try:
s2s_result = tx.s2s_do_transaction()
valid_state = 'authorized' if tx.acquirer_id.capture_manually else 'done'
if not s2s_result or tx.state != valid_state:
return dict(success=False, error=_("Payment transaction failed (%s)") % tx.state_message)
else:
# Auto-confirm SO if necessary
tx._confirm_so()
return dict(success=True, url='/shop/payment/validate')
except Exception as e:
_logger.warning(_("Payment transaction (%s) failed : <%s>") % (tx.id, str(e)))
return dict(success=False, error=_("Payment transaction failed (Contact Administrator)"))
return dict(success=False, error='Tx missmatch')
if not tx or not request.website.sale_get_transaction() or tx != request.website.sale_get_transaction():
return dict(success=False, error='Tx missmatch')
res = tx.confirm_sale_token()
if res is not True:
return dict(success=False, error=res)
return dict(success=True, url='/shop/payment/validate')
@http.route(['/shop/payment/transaction_token'], type='http', methods=['POST'], auth="public", website=True)
def payment_transaction_token(self, tx_id, **kwargs):
@@ -801,13 +790,12 @@ class WebsiteSale(http.Controller):
:param int acquirer_id: id of a payment.acquirer record. If not set the
user is redirected to the checkout page
"""
Transaction = request.env['payment.transaction'].sudo()
# In case the route is called directly from the JS (as done in Stripe payment method)
so_id = kwargs.get('so_id')
so_token = kwargs.get('so_token')
if so_id and so_token:
order = request.env['sale.order'].sudo().search([('id', '=', so_id), ('access_token', '=', so_token)])
access_token = kwargs.get('access_token')
if so_id and access_token:
order = request.env['sale.order'].sudo().search([('id', '=', so_id), ('access_token', '=', access_token)])
elif so_id:
order = request.env['sale.order'].search([('id', '=', so_id)])
else:
@@ -817,51 +805,17 @@ class WebsiteSale(http.Controller):
assert order.partner_id.id != request.website.partner_id.id
# find an already existing transaction
tx = request.website.sale_get_transaction()
if tx:
if tx.sale_order_id.id != order.id or tx.state in ['error', 'cancel'] or tx.acquirer_id.id != acquirer_id:
tx = False
elif token and tx.payment_token_id and token != tx.payment_token_id.id:
# new or distinct token
tx = False
elif tx.state == 'draft': # button cliked but no more info -> rewrite on tx or create a new one ?
tx.write(dict(Transaction.on_change_partner_id(order.partner_id.id).get('value', {}), amount=order.amount_total, type=tx_type))
if not tx:
tx_values = {
'acquirer_id': acquirer_id,
'type': tx_type,
'amount': order.amount_total,
'currency_id': order.pricelist_id.currency_id.id,
'partner_id': order.partner_id.id,
'partner_country_id': order.partner_id.country_id.id,
'reference': Transaction.get_next_reference(order.name),
'sale_order_id': order.id,
}
if token and request.env['payment.token'].sudo().browse(int(token)).partner_id == order.partner_id:
tx_values['payment_token_id'] = token
# find or create transaction
tx = request.website.sale_get_transaction() or request.env['payment.transaction'].sudo()
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
payment_token = request.env['payment.token'].sudo().browse(int(token)) if token else None
tx = tx.check_or_create_sale_tx(order, acquirer, payment_token=payment_token, tx_type=tx_type)
request.session['sale_transaction_id'] = tx.id
tx = Transaction.create(tx_values)
request.session['sale_transaction_id'] = tx.id
# update quotation
order.write({
'payment_acquirer_id': acquirer_id,
'payment_tx_id': request.session['sale_transaction_id']
})
if token:
return request.env.ref('website_sale.payment_token_form').render(dict(tx=tx), engine='ir.qweb')
return tx.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=_('Pay Now')).sudo().render(
tx.reference,
order.amount_total,
order.pricelist_id.currency_id.id,
values={
'return_url': '/shop/payment/validate',
'partner_id': order.partner_shipping_id.id or order.partner_invoice_id.id,
'billing_partner_id': order.partner_invoice_id.id,
},
)
return tx.render_sale_button(order, '/shop/payment/validate')
@http.route('/shop/payment/get_status/<int:sale_order_id>', type='json', auth="public", website=True)
def payment_get_status(self, sale_order_id, **post):
-2
View File
@@ -20,8 +20,6 @@ class SaleOrder(models.Model):
help='Order Lines to be displayed on the website. They should not be used for computation purpose.',
)
cart_quantity = fields.Integer(compute='_compute_cart_info', string='Cart Quantity')
payment_acquirer_id = fields.Many2one('payment.acquirer', string='Payment Acquirer', copy=False)
payment_tx_id = fields.Many2one('payment.transaction', string='Last Transaction', copy=False)
only_services = fields.Boolean(compute='_compute_cart_info', string='Only Services')
can_directly_mark_as_paid = fields.Boolean(compute='_compute_can_directly_mark_as_paid',
string="Can be directly marked as paid", store=True,
@@ -1,84 +1,13 @@
odoo.define('website_sale.payment', function (require) {
"use strict";
var ajax = require('web.ajax');
$(document).ready(function () {
// If option is enable
if ($("#checkbox_cgv").length) {
$("#checkbox_cgv").click(function() {
$("div.oe_sale_acquirer_button").find('input, button').prop("disabled", !this.checked);
$("div.o_payment_acquirer_button").find('input, button').prop("disabled", !this.checked);
});
}
// When choosing an acquirer, display its Pay Now button
var $payment = $("#payment_method");
$payment.on("click", "input[name='acquirer'], a.btn_payment_token", function (ev) {
var ico_off = 'fa-circle-o';
var ico_on = 'fa-dot-circle-o';
var payment_id = $(ev.currentTarget).val() || $(this).data('acquirer');
var token = $(ev.currentTarget).data('token') || '';
$("div.oe_sale_acquirer_button[data-id='"+payment_id+"']", $payment).attr('data-token', token);
$("div.js_payment a.list-group-item").removeClass("list-group-item-info");
$('span.js_radio').switchClass(ico_on, ico_off, 0);
if (token) {
$("div.oe_sale_acquirer_button div.token_hide").hide();
$(ev.currentTarget).find('span.js_radio').switchClass(ico_off, ico_on, 0);
$(ev.currentTarget).parents('li').find('input').prop("checked", true);
$(ev.currentTarget).addClass("list-group-item-info");
}
else{
$("div.oe_sale_acquirer_button div.token_hide").show();
}
$("div.oe_sale_acquirer_button[data-id]", $payment).addClass("hidden");
$("div.oe_sale_acquirer_button[data-id='"+payment_id+"']", $payment).removeClass("hidden");
})
.find("input[name='acquirer']:checked").click();
// When clicking on payment button: create the tx using json then continue to the acquirer
$payment.on("click", 'button[type="submit"], button[name="submit"]', function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $form = $(ev.currentTarget).parents('form');
var acquirer = $(ev.currentTarget).parents('div.oe_sale_acquirer_button').first();
var acquirer_id = acquirer.data('id');
var acquirer_token = acquirer.attr('data-token'); // !=data
var params = {'tx_type': acquirer.find('input[name="odoo_save_token"]').is(':checked')?'form_save':'form'};
if (! acquirer_id) {
return false;
}
if (acquirer_token) {
params.token = acquirer_token;
}
$form.off('submit');
ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', params).then(function (data) {
$(data).appendTo('body').submit();
});
return false;
});
$('div.oe_pay_token').on('click', 'a.js_btn_valid_tx', function() {
$('div.js_token_load').toggle();
var $form = $(this).parents('form');
ajax.jsonRpc($form.attr('action'), 'call', $.deparam($form.serialize())).then(function (data) {
if (data.url) {
window.location = data.url;
}
else {
$('div.js_token_load').toggle();
if (!data.success && data.error) {
$('div.oe_pay_token div.panel-body p').html(data.error + "<br/><br/>" + _('Retry ? '));
$('div.oe_pay_token div.panel-body').parents('div').removeClass('panel-info').addClass('panel-danger');
}
}
});
});
});
});
@@ -71,7 +71,7 @@ tour.register('shop_buy_product', {
{
content: "Pay Now",
extra_trigger: '#payment_method label:contains("Wire Transfer") input:checked',
trigger: '.oe_sale_acquirer_button .btn[type="submit"]:visible',
trigger: '.o_payment_acquirer_button .btn[type="submit"]:visible',
},
{
content: "finish",
+3 -1
View File
@@ -1424,7 +1424,9 @@
</div>
<div class="col-lg-5 col-sm-4 text-right">
<t t-foreach="acquirers or []" t-as="acquirer" id="acquirers_list">
<div t-att-data-id="acquirer.id" class="oe_sale_acquirer_button hidden pull-right">
<div class="o_payment_acquirer_button hidden pull-right"
t-att-data-id="acquirer.id"
t-att-data-call-url="'/shop/payment/transaction/%s' % (acquirer.id)">
<div t-raw="acquirer.button" />
<div class="token_hide">
<div t-if="acquirer.save_token == 'ask'">