[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:
@@ -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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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({
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -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}&amount=#{invoice.residual}&currency_id=#{invoice.currency_id.id}&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
|
||||
|
||||
@@ -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,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,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}&amount=#{invoice.residual}&currency_id=#{invoice.currency_id.id}&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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'">
|
||||
|
||||
Reference in New Issue
Block a user