[MERGE] forward port of branch saas-17 up to b970bf865a
This commit is contained in:
@@ -88,7 +88,8 @@ class StockMove(models.Model):
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
if any(move.quantity_done for move in self): #TODO: either put in stock, or check there is a production order related to it
|
||||
raise exceptions.UserError(_('You cannot cancel a stock move having already consumed material'))
|
||||
raise exceptions.UserError(_('You cannot cancel a manufacturing order if you have already consumed material.\
|
||||
If you want to cancel this MO, please change the consumed quantities to 0.'))
|
||||
return super(StockMove, self).action_cancel()
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -18,16 +18,9 @@ class WebsitePayment(http.Controller):
|
||||
}
|
||||
return_url = request.params.get('redirect', '/my/payment_method')
|
||||
for acquirer in acquirers:
|
||||
acquirer.form = acquirer.sudo()._registration_render(request.env.user.partner_id.id, {'error': {}, 'error_message': [], 'return_url': return_url, 'json': False, 'bootstrap_formatting': True, 'verify_validity': True})
|
||||
acquirer.form = acquirer.sudo()._registration_render(request.env.user.partner_id.id, {'error': {}, 'error_message': [], 'return_url': return_url, 'json': False, 'bootstrap_formatting': True})
|
||||
return request.render("payment.pay_methods", values)
|
||||
|
||||
@http.route(['/website_payment/delete/'], methods=['POST'], type='http', auth="user", website=True)
|
||||
def delete(self, delete_pm_id=None):
|
||||
if delete_pm_id:
|
||||
pay_meth = request.env['payment.token'].browse(int(delete_pm_id))
|
||||
pay_meth.unlink()
|
||||
return request.redirect('/my/payment_method')
|
||||
|
||||
@http.route(['/website_payment/pay'], type='http', auth='public', website=True)
|
||||
def pay(self, reference='', amount=False, currency_id=None, acquirer_id=None, **kw):
|
||||
env = request.env
|
||||
@@ -90,25 +83,3 @@ class WebsitePayment(http.Controller):
|
||||
return request.render('payment.confirm', {'tx': tx, 'status': status, 'message': message})
|
||||
else:
|
||||
return request.redirect('/my/home')
|
||||
|
||||
@http.route(['/website_payment/get_linked_records'], type='json', auth='user', website=True)
|
||||
def get_linked_records(self, payment_token_id):
|
||||
payment_token_id = int(payment_token_id)
|
||||
|
||||
res = []
|
||||
token = request.env['payment.token'].search([('id','=',payment_token_id)])
|
||||
# if the payment token is valid
|
||||
if token:
|
||||
# then we retrieve the description for the model sale.subscription
|
||||
subscription_description = request.env['sale.subscription']._description
|
||||
# we retrieve all the linked records for the token
|
||||
records = token._get_linked_records()
|
||||
# transform the list of tuple into a dict so we can search easily
|
||||
records = dict(records[payment_token_id])
|
||||
# if there's subscriptions linked to that token
|
||||
if records.get(subscription_description):
|
||||
subscriptions = records[subscription_description]
|
||||
# we create our json with all the info needed
|
||||
for r in subscriptions:
|
||||
res.append({'id': r.id, 'name': r.name})
|
||||
return res
|
||||
@@ -786,7 +786,7 @@ class PaymentToken(models.Model):
|
||||
return False
|
||||
|
||||
reference = "VALIDATION-%s-%s" % (self.id, datetime.datetime.now().strftime('%y%m%d_%H%M%S'))
|
||||
tx = self.env['payment.transaction'].create({
|
||||
tx = self.env['payment.transaction'].sudo().create({
|
||||
'amount': amount,
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'type': 'validation',
|
||||
@@ -816,6 +816,17 @@ class PaymentToken(models.Model):
|
||||
token.short_name = token.name.replace('XXXXXXXXXXXX', '***')
|
||||
|
||||
@api.multi
|
||||
def _get_linked_records(self):
|
||||
""" Returns the list of records currently using the records in self as payment token. """
|
||||
return {}
|
||||
def get_linked_records(self):
|
||||
""" This method returns a dict containing all the records linked to the payment.token (e.g Subscriptions),
|
||||
the key is the id of the payment.token and the value is an array that must follow the scheme below.
|
||||
|
||||
{
|
||||
token_id: [
|
||||
'description': The model description (e.g 'Sale Subscription'),
|
||||
'id': The id of the record,
|
||||
'name': The name of the record,
|
||||
'url': The url to access to this record.
|
||||
]
|
||||
}
|
||||
"""
|
||||
return {r.id:[] for r in self}
|
||||
|
||||
@@ -8,7 +8,7 @@ div.card_placeholder {
|
||||
width: 32px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 34px;
|
||||
top: 8px;
|
||||
right: 20px;
|
||||
-webkit-transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
||||
transition: 0.4s cubic-bezier(0.455, 0.03, 0.515, 0.955);
|
||||
|
||||
@@ -10,7 +10,7 @@ div.card_placeholder
|
||||
width: 32px
|
||||
height: 20px
|
||||
position: absolute
|
||||
top: 34px
|
||||
top: 8px
|
||||
right: 20px
|
||||
-webkit-transition: .4s cubic-bezier(0.455,0.03,0.515,0.955)
|
||||
transition: .4s cubic-bezier(0.455,0.03,0.515,0.955)
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
odoo.define('payment.payment_form', function (require){
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Dialog = require("web.Dialog");
|
||||
var Widget = require("web.Widget");
|
||||
var rpc = require("web.rpc");
|
||||
var _t = core._t;
|
||||
|
||||
var PaymentForm = Widget.extend({
|
||||
|
||||
events: {
|
||||
'click #o_payment_form_pay': 'payEvent',
|
||||
'click #o_payment_form_add_pm': 'addPmEvent',
|
||||
'click button[name="delete_pm"]': 'deletePmEvent',
|
||||
'click input[type="radio"]': 'radioClickEvent'
|
||||
},
|
||||
|
||||
payEvent: function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var form = this.el;
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
var self = this;
|
||||
var button = ev.target;
|
||||
|
||||
// first we check that the user has selected a payment method
|
||||
if(checked_radio.length == 1) {
|
||||
checked_radio = checked_radio[0];
|
||||
// if the user is adding a new payment
|
||||
if(this.isNewPaymentRadio(checked_radio)) {
|
||||
var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
|
||||
var acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id);
|
||||
// we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array
|
||||
var form_data = this.getFormData($('input', acquirer_form));
|
||||
var ds = $('input[name="data_set"]', acquirer_form)[0];
|
||||
$(button).attr('disabled', true);
|
||||
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i> </span>');
|
||||
|
||||
var verify_validity = this.$el.find('input[name="verify_validity"]');
|
||||
|
||||
if(verify_validity.length>0)
|
||||
form_data.verify_validity = verify_validity[0].value === "1";
|
||||
|
||||
// do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route'
|
||||
ajax.jsonRpc(ds.dataset.createRoute, 'call', form_data).then(function(data) {
|
||||
// if the server has returned true
|
||||
if(data.result) {
|
||||
// and it need a 3DS authentication
|
||||
if(data['3d_secure'] !== false) {
|
||||
// then we display the 3DS page to the user
|
||||
$("body").html(data['3d_secure']);
|
||||
}
|
||||
else {
|
||||
checked_radio.value = data.id; // set the radio value to the new card id
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
// if the server has returned false, we display an error
|
||||
else {
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>"));
|
||||
}
|
||||
// here we remove the 'processing' icon from the 'add a new payment' button
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
}).fail(function(message, data) {
|
||||
// if the rpc fails, pretty obvious
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>") + (core.debug ? data.data.message : ''));
|
||||
});
|
||||
}
|
||||
// if the user is using an old payment
|
||||
else {
|
||||
// then we just submit the form
|
||||
form.submit();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.error(_t('Error'),
|
||||
_t('<p>Please select a payment method</p>'));
|
||||
}
|
||||
},
|
||||
// event handler when clicking on the button to add a new payment method
|
||||
addPmEvent: function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var form = this.el;
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
var self = this;
|
||||
var button = ev.target;
|
||||
|
||||
// we check if the user has selected a 'add a new payment' option
|
||||
if(checked_radio.length == 1 && this.isNewPaymentRadio(checked_radio[0])) {
|
||||
// then we add a 'processing' icon into the 'add a new payment' button
|
||||
$(button).attr('disabled', true);
|
||||
$(button).prepend('<span class="o_loader"><i class="fa fa-refresh fa-spin"></i> </span>');
|
||||
// we retrieve which acquirer is used
|
||||
checked_radio = checked_radio[0];
|
||||
var acquirer_id = this.getAcquirerIdFromRadio(checked_radio);
|
||||
var acquirer_form = this.$('#o_payment_add_token_acq_' + acquirer_id);
|
||||
// we retrieve all the input inside the acquirer form and 'serialize' them to an indexed array
|
||||
var form_data = this.getFormData($('input', acquirer_form));
|
||||
var ds = $('input[name="data_set"]', acquirer_form)[0];
|
||||
|
||||
// we force the check when adding a card trough here
|
||||
form_data.verify_validity = true;
|
||||
|
||||
// do the call to the route stored in the 'data_set' input of the acquirer form, the data must be called 'create-route'
|
||||
ajax.jsonRpc(ds.dataset.createRoute, 'call', form_data).then(function(data) {
|
||||
// if the server has returned true
|
||||
if(data.result) {
|
||||
// and it need a 3DS authentication
|
||||
if(data['3d_secure'] !== false) {
|
||||
// then we display the 3DS page to the user
|
||||
$("body").html(data['3d_secure']);
|
||||
}
|
||||
// if it doesn't require 3DS
|
||||
else {
|
||||
// we just go to the return_url or reload the page
|
||||
if(form_data.return_url) {
|
||||
window.location = form_data.return_url;
|
||||
}
|
||||
else {
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
// if the server has returned false, we display an error
|
||||
else {
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>"));
|
||||
}
|
||||
// here we remove the 'processing' icon from the 'add a new payment' button
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
}).fail(function(message, data) {
|
||||
// if the rpc fails, pretty obvious
|
||||
$(button).attr('disabled', false);
|
||||
$(button).find('span.o_loader').remove();
|
||||
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to add your payment method at the moment.</p>") + (core.debug ? data.data.message : ''));
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.error(_t('Error'),
|
||||
_t('<p>Please select the option to add a new payment method</p>'));
|
||||
}
|
||||
},
|
||||
// event handler when clicking on a button to delete a payment method
|
||||
deletePmEvent: function (ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
var pm_id = parseInt(ev.target.value);
|
||||
|
||||
var Delete = function() {
|
||||
rpc.query({
|
||||
model: 'payment.token',
|
||||
method: 'unlink',
|
||||
args: [pm_id],
|
||||
})
|
||||
.then(function(result){
|
||||
if(result === true) {
|
||||
ev.target.closest('div').remove();
|
||||
}
|
||||
},function(type,err){
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to delete your payment method at the moment.</p>"));
|
||||
});
|
||||
};
|
||||
|
||||
rpc.query({
|
||||
model: 'payment.token',
|
||||
method: 'get_linked_records',
|
||||
args: [pm_id],
|
||||
}).then(function(result){
|
||||
if(result[pm_id].length > 0) {
|
||||
// if there's records linked to this payment method
|
||||
var content = '';
|
||||
result[pm_id].forEach(function(sub) {
|
||||
content += '<p><a href="' + sub.url + '" title="' + _.str.escapeHTML(sub.description) + '">' + _.str.escapeHTML(sub.name) + '</a><p/>';
|
||||
});
|
||||
|
||||
content = $('<div>').html(_t('<p>This card is currently linked to the following records:<p/>') + content);
|
||||
// Then we display the list of the records and ask the user if he really want to remove the ppayment method.
|
||||
new Dialog(self, {
|
||||
title: _t('Warning!'),
|
||||
size: 'medium',
|
||||
$content: content,
|
||||
buttons: [
|
||||
{text: _t('Confirm Deletion'), classes: 'btn-primary', close: true, click: Delete},
|
||||
{text: _t('Cancel'), close: true}]}).open();
|
||||
}
|
||||
else {
|
||||
// if there's no records linked to this payment method, then we delete it
|
||||
Delete();
|
||||
}
|
||||
}, function(type, err){
|
||||
self.error(_t('Error'),
|
||||
_t("<p>We are not able to delete your payment method at the moment.</p>") + (core.debug ? err.data.message : ''));
|
||||
});
|
||||
},
|
||||
// event handler when clicking on a radio button
|
||||
radioClickEvent: function(ev) {
|
||||
this.updateNewPaymentDisplayStatus();
|
||||
},
|
||||
updateNewPaymentDisplayStatus: function()
|
||||
{
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
// we hide all the acquirers form
|
||||
this.$('[id*="o_payment_add_token_acq_"]').addClass('hidden');
|
||||
// if we clicked on an add new payment radio
|
||||
if(checked_radio.length == 1 && this.isNewPaymentRadio(checked_radio[0]))
|
||||
{
|
||||
// then we retrieve the acquirer name
|
||||
var acquirer_id = this.getAcquirerIdFromRadio(checked_radio[0]);
|
||||
// and display its form
|
||||
this.$('#o_payment_add_token_acq_' + acquirer_id).removeClass('hidden');
|
||||
}
|
||||
},
|
||||
isNewPaymentRadio: function(element) {
|
||||
return $(element).data('acquirer-id') !== undefined;
|
||||
},
|
||||
getAcquirerIdFromRadio: function(element) {
|
||||
return $(element).data('acquirer-id');
|
||||
},
|
||||
error: function(title, message, url) {
|
||||
return new Dialog(null, {
|
||||
title: title || "",
|
||||
$content: $(core.qweb.render('website.error_dialog', {
|
||||
message: message || "",
|
||||
})),
|
||||
}).open();
|
||||
},
|
||||
getFormData: function($form){
|
||||
var unindexed_array = $form.serializeArray();
|
||||
var indexed_array = {};
|
||||
|
||||
$.map(unindexed_array, function(n, i){
|
||||
indexed_array[n.name] = n.value;
|
||||
});
|
||||
return indexed_array;
|
||||
},
|
||||
start: function() {
|
||||
this.updateNewPaymentDisplayStatus();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
$(document).ready(function (){
|
||||
var form = new PaymentForm();
|
||||
form.attachTo($('.o_payment_form'));
|
||||
});
|
||||
});
|
||||
@@ -60,64 +60,3 @@ $(document).ready(function () {
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
odoo.define('payment.payment_acquirer', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
if( !$('#payment_tokens_list').length ) {
|
||||
return $.Deferred().reject("DOM doesn't contain '.payment_tokens_list'");
|
||||
}
|
||||
|
||||
var payment_methods = $('#payment_tokens_list');
|
||||
|
||||
payment_methods.find('button[name="delete_pm_id"]').on('click', function(ev){
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var self = this;
|
||||
|
||||
// we retrieve the selected payment method id
|
||||
var payment_method_id = ev.target.value;
|
||||
|
||||
var Delete = function()
|
||||
{
|
||||
payment_methods.find('button[name="delete_pm_id"]').off("click");
|
||||
ev.target.click();
|
||||
};
|
||||
|
||||
ajax.jsonRpc('/website_payment/get_linked_records', 'call', {
|
||||
'payment_token_id': payment_method_id,
|
||||
}).done(function (response) {
|
||||
// if there's records linked to the payment method we're trying to delete
|
||||
if(response.length > 0)
|
||||
{
|
||||
var content = '';
|
||||
|
||||
response.forEach(function(sub) {
|
||||
content += '<p><a href="/my/subscription/' + sub.id + '">' + sub.name + '</a><p/>';
|
||||
});
|
||||
|
||||
content = $('<div>').html('<p>This card is currently linked to the following records:<p/>' + content);
|
||||
// Then we display the list of the records and ask the user if he really want to remove the ppayment method.
|
||||
new Dialog(self, {
|
||||
title: 'Warning!',
|
||||
size: 'medium',
|
||||
$content: content,
|
||||
buttons: [
|
||||
{text: 'Confirm Deletion', classes: 'btn-primary', close: true, click: Delete},
|
||||
{text: 'Cancel', close: true}]}).open();
|
||||
}
|
||||
// otherwise if the user has no records linked to this payment method, then we just delete it.
|
||||
else
|
||||
{
|
||||
Delete();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
.o_payment_form {
|
||||
label > input[type="radio"]{
|
||||
vertical-align: bottom;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: normal !important;
|
||||
font-family: Helvetica Neue, sans-serif;
|
||||
line-height: 1.3em;
|
||||
color: #4d4d4d;
|
||||
}
|
||||
|
||||
.panel-body:first-child {
|
||||
border-top: 0px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.panel-footer:last-child {
|
||||
border-bottom-right-radius: 10px !important;
|
||||
border-bottom-left-radius: 10px !important;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
border-top: 1px solid #ddd;
|
||||
padding: 1.14em !important;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_transaction.js"></script>
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_portal.js"></script>
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_transaction_portal.js"></script>
|
||||
<script type="text/javascript" src="/payment/static/src/js/payment_form.js"></script>
|
||||
<link rel="stylesheet" href="/payment/static/src/less/payment_form.less"/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -1,45 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="pay_methods" name="Payment Acquirers">
|
||||
<template id="pay_methods" name="Payment Methods">
|
||||
<t t-call="portal.frontend_layout">
|
||||
<t t-set="additional_title">Payment Acquirers</t>
|
||||
<t t-set="additional_title">Payment Methods</t>
|
||||
<div class="wrap">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<ol class="breadcrumb mt8">
|
||||
<li><a href="/my/home"><i class="fa fa-home"/></a></li>
|
||||
<li>Payment Acquirers</li>
|
||||
<li>Payment Methods</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<h1>Payment Acquirers</h1>
|
||||
<h4 t-if="acquirers">Add a new payment acquirer:</h4>
|
||||
<div t-if="acquirers" class="row">
|
||||
<div class="col-sm-8">
|
||||
<div t-if="error_message" class="alert alert-danger">
|
||||
<t t-foreach="error_message" t-as="err"><t t-esc="err"/><br /></t>
|
||||
<div class="clearfix">
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<h3>Manage your Payment Methods</h3>
|
||||
<t t-call="payment.payment_tokens_list">
|
||||
<t t-set="mode" t-value="'manage'"/>
|
||||
<t t-set="verify_validity" t-value="True"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if='len(acquirers)>1'>
|
||||
<label class="control-label" for="pm_acquirer_id">Acquirer</label>
|
||||
<select name="pm_acquirer_id" class="form-control">
|
||||
<t t-foreach="acquirers" t-as="acquirer">
|
||||
<option t-att-value="acquirer.id" t-esc="acquirer.name"/>
|
||||
</t>
|
||||
</select>
|
||||
</div>
|
||||
<t t-foreach="acquirers" t-as="acquirer">
|
||||
<div t-raw="acquirer.form" t-att-data-acquirer-id="acquirer.id" t-attf-class="acquirer #{'hidden' if acquirer!=acquirers[0] else ''}"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="pms" class="clearfix">
|
||||
<h4>Your payment Methods</h4>
|
||||
<t t-set="show_delete" t-value="True"/>
|
||||
<t t-set="form_action" t-value="'/website_payment/delete/'"/>
|
||||
<div class="col-sm-8">
|
||||
<t t-call="payment.payment_tokens_list"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,7 +34,7 @@
|
||||
<template id="pay_meth_link" inherit_id="portal.portal_layout">
|
||||
<xpath expr="//div[hasclass('o_portal_my_details')]" position="inside">
|
||||
<t t-if="request.env['payment.acquirer'].search([('website_published', '=', True), ('registration_view_template_id', '!=', False)])">
|
||||
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment acquirers</a></div>
|
||||
<div class='manage_payment_method mt16'><a href="/my/payment_method">Manage your payment methods</a></div>
|
||||
</t>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
@@ -9,59 +9,71 @@
|
||||
<template id="payment_tokens_list" name="Payment Tokens list">
|
||||
<!--
|
||||
Variables description:
|
||||
- 'payment' set this variable when you're doing a payment, this will allow to display the radio buttons for each tokens and show the submit button
|
||||
- 'show_delete' when you want to display the delete button for each token
|
||||
- 'show_radio' when you want to display the radio button for each token
|
||||
- 'show_submit' when you want to display the submit button at the bottom of the form
|
||||
- 'submit_txt' the text displayed inside the submit button
|
||||
- 'submit_class' the css classes to style the submit button
|
||||
- 'icon_class' font awesome class (e.g. 'fa-trash', 'fa-lock')
|
||||
- 'form_action' the URI to the page that will handle the form values
|
||||
- 'pms' the tokens
|
||||
- 'checked_pm_id' the payment token that should be checked (for radio buttons)
|
||||
- 'mode' can take two values, either 'payment' or 'manage'. 'manage' displays the add a new card and delete buttons. 'payment'
|
||||
display a form that is used to pay and send the information to the form action url.
|
||||
- 'acquirers' the list of the acquirers
|
||||
- 'verify_validity' if we need to verify if the payment method is valid when adding a new one
|
||||
-->
|
||||
<form id="payment_tokens_list" method="post" t-att-action="form_action if form_action else '#'">
|
||||
<form method="post" t-att-action="form_action if form_action else '#'" class="o_payment_form">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<table class="table table-hover table-striped table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Payment Methods</th>
|
||||
<th t-if="show_verification_status">Status</th>
|
||||
<th t-if="show_delete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<t t-foreach="pms" t-as="pm">
|
||||
<tr>
|
||||
<td>
|
||||
<t t-if="show_radio or payment">
|
||||
<div class="radio">
|
||||
<label><input type="radio" name="pm_id" t-att-value="pm.id" t-att-checked="checked_pm_id == pm.id"/><t t-esc="pm.short_name"/></label>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="1">
|
||||
<p><t t-esc="pm.short_name"/></p>
|
||||
</t>
|
||||
</td>
|
||||
<td t-if="show_verification_status"><t t-esc="'Verified' if pm.verified else 'Not Verified'"/></td>
|
||||
<td t-if="show_delete"><button type="submit" name="delete_pm_id" t-att-value="pm.id" class="btn btn-xs btn-primary pull-right"><i class="fa fa-trash"> Delete</i></button></td>
|
||||
</tr>
|
||||
<input type="hidden" t-if="verify_validity==True" name="verify_validity" value="1"/>
|
||||
<div class="panel panel-default">
|
||||
<t t-foreach="acquirers" t-as="acq">
|
||||
<div class="panel-body">
|
||||
<label>
|
||||
<t t-if="mode == 'manage'">
|
||||
<input type="radio" t-att-data-acquirer-id="acq.id" name="pm_id" t-attf-value="new_{{acq.id}}" t-att-class="'hidden' if (len(acquirers)==1) else ''" t-att-checked="len(acquirers) == 1"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<input type="radio" t-att-data-acquirer-id="acq.id" name="pm_id" t-attf-value="new_{{acq.id}}" t-att-class="'hidden' if (len(acquirers)==1 and len(pms)==0) else ''" t-att-checked="len(acquirers)==1 and len(pms)==0"/>
|
||||
</t>
|
||||
<t t-esc="acq.name"/>
|
||||
</label>
|
||||
</div>
|
||||
<div t-attf-id="o_payment_add_token_acq_{{acq.id}}" t-attf-class="panel-footer {{'hidden' if (len(acquirers)>1 and len(pms)==0 and acquirers[0]!=acq) else 'hidden' if len(pms)>0 else ''}}">
|
||||
<div class="clearfix">
|
||||
<t t-raw="acq.form" t-att-data-acquirer-id="acquirer.id"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="pms" t-as="pm">
|
||||
<div class="panel-body">
|
||||
<label>
|
||||
<input t-if="mode == 'payment'" type="radio" name="pm_id" t-att-value="pm.id" t-att-checked="checked_pm_id == pm.id"/>
|
||||
<t t-esc="pm.name"/>
|
||||
<t t-if="pm.verified">
|
||||
<i class="fa fa-check text-success" title="This payment method is verified by our system."></i>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<i class="fa fa-close text-danger" title="This payment method has not been verified by our system."></i>
|
||||
</t>
|
||||
</label>
|
||||
<button t-if="mode == 'manage'" name="delete_pm" t-att-value="pm.id" class="btn btn-primary btn-xs pull-right">
|
||||
<i class="fa fa-trash"></i> Delete
|
||||
</button>
|
||||
</div>
|
||||
</t>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
<button t-if="mode == 'payment'" id="o_payment_form_pay" type="submit" t-att-class="submit_class if submit_class else 'btn btn-primary btn-lg mb8 mt8'">
|
||||
<t t-if="submit_txt">
|
||||
<i t-if="icon_class" t-attf-class="fa {{icon_class}}"/>
|
||||
<t t-esc="submit_txt"/>
|
||||
</t>
|
||||
<tr t-if="(payment or show_submit) and not delete">
|
||||
<th t-if="show_verification_status"></th>
|
||||
<th t-if="show_delete"></th>
|
||||
<th>
|
||||
<button type="submit" t-att-class="submit_class if submit_class else 'btn btn-primary btn-sm mb8 mt8 pull-right'">
|
||||
<t t-if="submit_txt">
|
||||
<t t-raw="submit_txt"/>
|
||||
</t>
|
||||
<t t-else="1">
|
||||
<span class="fa fa-lock"></span> Pay
|
||||
</t>
|
||||
</button>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<t t-else="">
|
||||
<i class="fa fa-lock"> Pay</i>
|
||||
</t>
|
||||
</button>
|
||||
<button t-if="mode == 'manage'" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<i class="fa fa-plus-circle"> Add new card</i>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</odoo>
|
||||
|
||||
@@ -35,7 +35,31 @@ class AuthorizeController(http.Controller):
|
||||
def authorize_s2s_create_json(self, **kwargs):
|
||||
acquirer_id = int(kwargs.get('acquirer_id'))
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
return acquirer.s2s_process(kwargs)
|
||||
return acquirer.s2s_process(kwargs).id
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
'result': False,
|
||||
}
|
||||
return res
|
||||
|
||||
res = {
|
||||
'result': True,
|
||||
'id': token.id,
|
||||
'short_name': token.short_name,
|
||||
'3d_secure': False,
|
||||
'verified': False,
|
||||
}
|
||||
|
||||
if verify_validity != False:
|
||||
token.validate()
|
||||
res['verified'] = token.verified
|
||||
|
||||
return res
|
||||
|
||||
@http.route(['/payment/authorize/s2s/create'], type='http', auth='public')
|
||||
def authorize_s2s_create(self, **post):
|
||||
|
||||
@@ -163,6 +163,33 @@ class AuthorizeAPI():
|
||||
res['name'] = response_profile.find('paymentProfile/payment/creditCard/cardNumber').text
|
||||
return res
|
||||
|
||||
def credit(self, token, amount, transaction_id):
|
||||
""" Refund a payment for the given amount.
|
||||
|
||||
:param record token: the payment.token record that must be refunded.
|
||||
:param str amount: transaction amount
|
||||
:param str transaction_id: the reference of the transacation that is going to be refunded.
|
||||
|
||||
:return: a dict containing the response code, transaction id and transaction type
|
||||
:rtype: dict
|
||||
"""
|
||||
root = self._base_tree('createTransactionRequest')
|
||||
tx = etree.SubElement(root, "transactionRequest")
|
||||
etree.SubElement(tx, "transactionType").text = "refundTransaction"
|
||||
etree.SubElement(tx, "amount").text = str(amount)
|
||||
payment = etree.SubElement(tx, "payment")
|
||||
credit_card = etree.SubElement(payment, "creditCard")
|
||||
idx = token.name.find(' - ')
|
||||
etree.SubElement(credit_card, "cardNumber").text = token.name[idx-4:idx] # shitty hack, but that's the only way to get the 4 last digits
|
||||
etree.SubElement(credit_card, "expirationDate").text = "XXXX"
|
||||
etree.SubElement(tx, "refTransId").text = transaction_id
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = transaction_id
|
||||
res['x_type'] = 'refund'
|
||||
return res
|
||||
|
||||
# Transaction management
|
||||
def auth_and_capture(self, token, amount, reference):
|
||||
"""Authorize and capture a payment for the given amount.
|
||||
|
||||
@@ -116,7 +116,7 @@ class PaymentAcquirerAuthorize(models.Model):
|
||||
'partner_id': int(data.get('partner_id'))
|
||||
}
|
||||
PaymentMethod = self.env['payment.token'].sudo().create(values)
|
||||
return PaymentMethod.id
|
||||
return PaymentMethod
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_form_validate(self, data):
|
||||
@@ -191,7 +191,7 @@ class TxAuthorize(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _authorize_form_validate(self, data):
|
||||
if self.state == 'done':
|
||||
if self.state in ['done', 'refunded']:
|
||||
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
|
||||
return True
|
||||
status_code = int(data.get('x_response_code', '0'))
|
||||
@@ -219,6 +219,9 @@ class TxAuthorize(models.Model):
|
||||
'partner_id': self.partner_id.id,
|
||||
})
|
||||
self.payment_token_id = token_id
|
||||
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
return True
|
||||
elif status_code == self._authorize_pending_tx_status:
|
||||
self.write({
|
||||
@@ -253,6 +256,17 @@ class TxAuthorize(models.Model):
|
||||
res = transaction.authorize(self.payment_token_id, self.amount, self.reference)
|
||||
return self._authorize_s2s_validate_tree(res)
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_do_refund(self):
|
||||
self.ensure_one()
|
||||
transaction = AuthorizeAPI(self.acquirer_id)
|
||||
self.state = 'refunding'
|
||||
if self.type == 'validation':
|
||||
res = transaction.void(self.acquirer_reference)
|
||||
else:
|
||||
res = transaction.credit(self.payment_token_id, self.amount, self.acquirer_reference)
|
||||
return self._authorize_s2s_validate_tree(res)
|
||||
|
||||
@api.multi
|
||||
def authorize_s2s_capture_transaction(self):
|
||||
self.ensure_one()
|
||||
@@ -273,8 +287,7 @@ class TxAuthorize(models.Model):
|
||||
|
||||
@api.multi
|
||||
def _authorize_s2s_validate(self, tree):
|
||||
self.ensure_one()
|
||||
if self.state == 'done':
|
||||
if self.state in ['done', 'refunded']:
|
||||
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
|
||||
return True
|
||||
status_code = int(tree.get('x_response_code', '0'))
|
||||
@@ -288,6 +301,10 @@ class TxAuthorize(models.Model):
|
||||
})
|
||||
if init_state != 'authorized':
|
||||
self.execute_callback()
|
||||
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
|
||||
if tree.get('x_type').lower() == 'auth_only':
|
||||
self.write({
|
||||
'state': 'authorized',
|
||||
@@ -295,13 +312,19 @@ class TxAuthorize(models.Model):
|
||||
})
|
||||
self.execute_callback()
|
||||
if tree.get('x_type').lower() == 'void':
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
})
|
||||
if self.type == 'validation' and self.state == 'refunding':
|
||||
self.write({
|
||||
'state': 'refunded',
|
||||
})
|
||||
else:
|
||||
self.write({
|
||||
'state': 'cancel',
|
||||
})
|
||||
return True
|
||||
elif status_code == self._authorize_pending_tx_status:
|
||||
new_state = 'refunding' if self.state == 'refunding' else 'pending'
|
||||
self.write({
|
||||
'state': 'pending',
|
||||
'state': new_state,
|
||||
'acquirer_reference': tree.get('x_trans_id'),
|
||||
})
|
||||
return True
|
||||
|
||||
@@ -54,38 +54,28 @@
|
||||
</template>
|
||||
|
||||
<template id="authorize_s2s_form">
|
||||
<form method="post" t-att-action="'/payment/authorize/s2s/create' if not json else '/payment/authorize/s2s/create_json'">
|
||||
<div t-att-class="'row' if bootstrap_formatting else ''">
|
||||
<div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_number">Card number</label>
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="•••• •••• •••• •••• "/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_cvc">CVC</label>
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_holder_name">Name</label>
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_expires_mm">Expires</label>
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input class="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
<div class="clearfix"/>
|
||||
<input type="hidden" name="data_set" data-create-route="/payment/authorize/s2s/create_json_3ds"/>
|
||||
<div class="mt8">
|
||||
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number"/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<button t-att-class="submit_class" class="btn btn-primary" type="submit">
|
||||
<span t-if="not submit_txt">Confirm <span class="fa fa-long-arrow-right"></span></span>
|
||||
<span t-if="submit_txt"><t t-esc="submit_txt"/></span>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-3' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input class="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -33,6 +33,40 @@ class OgoneController(http.Controller):
|
||||
new_id = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
return new_id.id
|
||||
|
||||
@http.route(['/payment/ogone/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def ogone_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
'result': False,
|
||||
}
|
||||
return res
|
||||
|
||||
res = {
|
||||
'result': True,
|
||||
'id': token.id,
|
||||
'short_name': token.short_name,
|
||||
'3d_secure': False,
|
||||
'verified': False,
|
||||
}
|
||||
|
||||
if verify_validity != False:
|
||||
baseurl = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
params = {
|
||||
'accept_url': baseurl + '/payment/ogone/validate/accept',
|
||||
'decline_url': baseurl + '/payment/ogone/validate/decline',
|
||||
'exception_url': baseurl + '/payment/ogone/validate/exception',
|
||||
'return_url': kwargs.get('return_url', baseurl)
|
||||
}
|
||||
tx = token.validate(**params)
|
||||
res['verified'] = token.verified
|
||||
|
||||
if tx and tx.html_3ds:
|
||||
res['3d_secure'] = tx.html_3ds
|
||||
|
||||
return res
|
||||
|
||||
@http.route(['/payment/ogone/s2s/create'], type='http', auth='public', methods=["POST"], csrf=False)
|
||||
def ogone_s2s_create(self, **post):
|
||||
error = ''
|
||||
|
||||
@@ -302,10 +302,11 @@ class PaymentTxOgone(models.Model):
|
||||
})
|
||||
vals.update(payment_token_id=pm.id)
|
||||
self.write(vals)
|
||||
self.payment_token_id.verified = True
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
self.execute_callback()
|
||||
# if this transaction is a validation one, then we refund the money we just withdrawn
|
||||
if self.type == 'verification':
|
||||
if self.type == 'validation':
|
||||
self.s2s_do_refund()
|
||||
|
||||
return True
|
||||
@@ -454,7 +455,8 @@ class PaymentTxOgone(models.Model):
|
||||
'name': tree.get('CARDNO'),
|
||||
})
|
||||
self.write({'payment_token_id': pm.id})
|
||||
self.payment_token_id.verified = True
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
self.execute_callback()
|
||||
return True
|
||||
elif status in self._ogone_cancel_tx_status:
|
||||
@@ -463,7 +465,7 @@ class PaymentTxOgone(models.Model):
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
})
|
||||
elif status in self._ogone_pending_tx_status:
|
||||
new_state = 'refunding' if self.type == 'validation' else 'pending'
|
||||
new_state = 'refunding' if self.state == 'refunding' else 'pending'
|
||||
self.write({
|
||||
'state': new_state,
|
||||
'acquirer_reference': tree.get('PAYID'),
|
||||
|
||||
@@ -54,40 +54,27 @@
|
||||
</template>
|
||||
|
||||
<template id="ogone_s2s_form">
|
||||
<form method="post" t-att-action="'/payment/ogone/s2s/create' if not json else '/payment/ogone/s2s/create_json'">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input t-if="verify_validity==True" type="hidden" name="verify_validity" value="1"/>
|
||||
<div t-att-class="'row' if bootstrap_formatting else None">
|
||||
<div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_number">Card number</label>
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="•••• •••• •••• •••• "/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_cvc">CVC</label>
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_holder_name">Name</label>
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_expires_mm">Expires</label>
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
<div class="clearfix"/>
|
||||
<input type="hidden" name="data_set" data-create-route="/payment/ogone/s2s/create_json_3ds"/>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
<div class="mt8">
|
||||
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number"/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<button t-att-class="submit_class" class="btn btn-primary" type="submit">
|
||||
<span t-if="not submit_txt">Confirm <span class="fa fa-long-arrow-right"></span></span>
|
||||
<span t-if="submit_txt"><t t-esc="submit_txt"/></span>
|
||||
</button>
|
||||
</form>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-3' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -15,7 +15,7 @@ class StripeController(http.Controller):
|
||||
def stripe_s2s_create_json(self, **kwargs):
|
||||
acquirer_id = int(kwargs.get('acquirer_id'))
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
return acquirer.s2s_process(kwargs)
|
||||
return acquirer.s2s_process(kwargs).id
|
||||
|
||||
@http.route(['/payment/stripe/s2s/create'], type='http', auth='public')
|
||||
def stripe_s2s_create(self, **post):
|
||||
@@ -24,6 +24,30 @@ class StripeController(http.Controller):
|
||||
acquirer.s2s_process(post)
|
||||
return werkzeug.utils.redirect(post.get('return_url', '/'))
|
||||
|
||||
@http.route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
|
||||
|
||||
if not token:
|
||||
res = {
|
||||
'result': False,
|
||||
}
|
||||
return res
|
||||
|
||||
res = {
|
||||
'result': True,
|
||||
'id': token.id,
|
||||
'short_name': token.short_name,
|
||||
'3d_secure': False,
|
||||
'verified': False,
|
||||
}
|
||||
|
||||
if verify_validity != False:
|
||||
token.validate()
|
||||
res['verified'] = token.verified
|
||||
|
||||
return res
|
||||
|
||||
@http.route(['/payment/stripe/create_charge'], type='json', auth='public')
|
||||
def stripe_create_charge(self, **post):
|
||||
""" Create a payment transaction
|
||||
|
||||
@@ -71,7 +71,7 @@ class PaymentAcquirerStripe(models.Model):
|
||||
'acquirer_id': int(data['acquirer_id']),
|
||||
'partner_id': int(data['partner_id'])
|
||||
})
|
||||
return payment_token.id
|
||||
return payment_token
|
||||
|
||||
@api.multi
|
||||
def stripe_s2s_form_validate(self, data):
|
||||
@@ -83,6 +83,21 @@ class PaymentAcquirerStripe(models.Model):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
Each provider should add its technical in the corresponding
|
||||
key for the following features:
|
||||
* fees: support payment fees computations
|
||||
* authorize: support authorizing payment (separates
|
||||
authorization and capture)
|
||||
* tokenize: support saving payment data in a payment.tokenize
|
||||
object
|
||||
"""
|
||||
res = super(PaymentAcquirerStripe, self)._get_feature_support()
|
||||
res['tokenize'].append('stripe')
|
||||
return res
|
||||
|
||||
|
||||
class PaymentTransactionStripe(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
@@ -112,6 +127,29 @@ class PaymentTransactionStripe(models.Model):
|
||||
result = self._create_stripe_charge(acquirer_ref=self.payment_token_id.acquirer_ref)
|
||||
return self._stripe_s2s_validate_tree(result)
|
||||
|
||||
|
||||
def _create_stripe_refund(self):
|
||||
api_url_refund = 'https://%s/refunds' % (self.acquirer_id._get_stripe_api_url())
|
||||
|
||||
refund_params = {
|
||||
'charge': self.acquirer_reference,
|
||||
'amount': int(self.amount*100), # by default, stripe refund the full amount (we don't really need to specify the value)
|
||||
'metadata[reference]': self.reference,
|
||||
}
|
||||
|
||||
r = requests.post(api_url_refund,
|
||||
auth=(self.acquirer_id.stripe_secret_key, ''),
|
||||
params=refund_params,
|
||||
headers=STRIPE_HEADERS)
|
||||
return r.json()
|
||||
|
||||
@api.multi
|
||||
def stripe_s2s_do_refund(self, **kwargs):
|
||||
self.ensure_one()
|
||||
self.state = 'refunding'
|
||||
result = self._create_stripe_refund()
|
||||
return self._stripe_s2s_validate_tree(result)
|
||||
|
||||
@api.model
|
||||
def _stripe_form_get_tx_from_data(self, data):
|
||||
""" Given a data dict coming from stripe, verify it and find the related
|
||||
@@ -135,18 +173,21 @@ class PaymentTransactionStripe(models.Model):
|
||||
@api.multi
|
||||
def _stripe_s2s_validate_tree(self, tree):
|
||||
self.ensure_one()
|
||||
if self.state not in ('draft', 'pending'):
|
||||
if self.state not in ('draft', 'pending', 'refunding'):
|
||||
_logger.info('Stripe: trying to validate an already validated tx (ref %s)', self.reference)
|
||||
return True
|
||||
|
||||
status = tree.get('status')
|
||||
if status == 'succeeded':
|
||||
new_state = 'refunded' if self.state == 'refunding' else 'done'
|
||||
self.write({
|
||||
'state': 'done',
|
||||
'state': new_state,
|
||||
'date_validate': fields.datetime.now(),
|
||||
'acquirer_reference': tree.get('id'),
|
||||
})
|
||||
self.execute_callback()
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
return True
|
||||
else:
|
||||
error = tree['error']['message']
|
||||
|
||||
@@ -32,38 +32,29 @@
|
||||
</template>
|
||||
|
||||
<template id="stripe_s2s_form">
|
||||
<form method="post" t-att-action="'/payment/stripe/s2s/create' if not json else '/payment/stripe/s2s/create_json'">
|
||||
<div t-att-class="'row' if bootstrap_formatting else None">
|
||||
<div t-att-class="'form-group col-md-8' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_number">Card Number</label>
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="•••• •••• •••• •••• "/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cvc">CVC</label>
|
||||
<input type="text" name="cvc" id="cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-7' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_holder_name">Card Holder Name</label>
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<label class="control-label" for="cc_expires_mm">Expiration Date</label>
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="MM / YY"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="clearfix"/>
|
||||
<input type="hidden" name="data_set" data-create-route="/payment/stripe/s2s/create_json_3ds"/>
|
||||
<div class="mt8">
|
||||
<div t-att-class="'form-group col-md-12' if bootstrap_formatting else 'form-group'">
|
||||
<input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number"/>
|
||||
<div class="card_placeholder"></div>
|
||||
<div class="visa"></div>
|
||||
<input type="hidden" name="cc_brand" value=""/>
|
||||
</div>
|
||||
<button t-att-class="submit_class" class="btn btn-primary" type="submit">
|
||||
<span t-if="not submit_txt">Confirm <span class="fa fa-long-arrow-right"></span></span>
|
||||
<span t-if="submit_txt"><t t-esc="submit_txt"/></span>
|
||||
</button>
|
||||
</form>
|
||||
<div t-att-class="'form-group col-md-5' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-3' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)"/>
|
||||
</div>
|
||||
<div t-att-class="'form-group col-md-4' if bootstrap_formatting else 'form-group'">
|
||||
<input type="text" name="cvc" id="cvc" class="form-control" maxlength="4" placeholder="CVC"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
|
||||
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="clearfix"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="payment_stripe_redirect" name="Payment Stripe">
|
||||
|
||||
@@ -85,7 +85,6 @@ class PurchaseOrder(models.Model):
|
||||
# We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
|
||||
# do some recursive search, but that could be prohibitive if not done correctly.
|
||||
moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
|
||||
moves = moves.filtered(lambda r: r.state != 'cancel')
|
||||
pickings |= moves.mapped('picking_id')
|
||||
order.picking_ids = pickings
|
||||
order.picking_count = len(pickings)
|
||||
@@ -666,12 +665,7 @@ class PurchaseOrderLine(models.Model):
|
||||
break
|
||||
else:
|
||||
diff_qty += move.product_qty
|
||||
move.do_unreserve()
|
||||
move.state = 'draft'
|
||||
picking = move.picking_id
|
||||
move.unlink()
|
||||
if not picking.move_lines:
|
||||
picking.unlink()
|
||||
move.action_cancel()
|
||||
else:
|
||||
raise UserError('You cannot decrease the ordered quantity below the received quantity.\n'
|
||||
'Create a return first.')
|
||||
|
||||
@@ -267,9 +267,9 @@ class Product(models.Model):
|
||||
domain_quant.append(('owner_id', '=', owner_id))
|
||||
if package_id:
|
||||
domain_quant.append(('package_id', '=', package_id))
|
||||
quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'qty'], ['product_id'])
|
||||
quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'quantity'], ['product_id'])
|
||||
for quant in quants_groupby:
|
||||
if OPERATORS[operator](quant['qty'], value):
|
||||
if OPERATORS[operator](quant['quantity'], value):
|
||||
product_ids.add(quant['product_id'][0])
|
||||
return list(product_ids)
|
||||
|
||||
|
||||
@@ -436,6 +436,7 @@ class StockMove(models.Model):
|
||||
show_source_location=self.location_id.child_ids,
|
||||
show_destination_location=self.location_dest_id.child_ids,
|
||||
show_package=not self.location_id.usage == 'supplier',
|
||||
show_reserved_quantity=self.state != 'done'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
from odoo.addons import decimal_precision as dp
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.pycompat import izip
|
||||
from odoo.tools.float_utils import float_round, float_compare, float_is_zero
|
||||
|
||||
@@ -14,14 +14,13 @@ class StockMoveLine(models.Model):
|
||||
_description = "Packing Operation"
|
||||
_order = "result_package_id desc, id"
|
||||
|
||||
|
||||
picking_id = fields.Many2one(
|
||||
'stock.picking', 'Stock Picking',
|
||||
help='The stock operation where the packing has been made')
|
||||
move_id = fields.Many2one(
|
||||
'stock.move', 'Stock Move',
|
||||
help="Change to a better name")
|
||||
product_id = fields.Many2one('product.product', 'Product', ondelete="cascade") #might be a related with the move also --> no, because you can put them next to each other
|
||||
'stock.move', 'Stock Move',
|
||||
help="Change to a better name")
|
||||
product_id = fields.Many2one('product.product', 'Product', ondelete="cascade")
|
||||
product_uom_id = fields.Many2one('product.uom', 'Unit of Measure', required=True)
|
||||
product_qty = fields.Float(
|
||||
'Real Reserved Quantity', digits=0,
|
||||
@@ -47,7 +46,6 @@ class StockMoveLine(models.Model):
|
||||
consume_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'consume_line_id', 'produce_line_id', help="Technical link to see who consumed what. ")
|
||||
produce_line_ids = fields.Many2many('stock.move.line', 'stock_move_line_consume_rel', 'produce_line_id', 'consume_line_id', help="Technical link to see which line was produced with this. ")
|
||||
|
||||
|
||||
@api.one
|
||||
def _compute_location_description(self):
|
||||
for operation, operation_sudo in izip(self, self.sudo()):
|
||||
@@ -76,6 +74,13 @@ class StockMoveLine(models.Model):
|
||||
detect errors. """
|
||||
raise UserError(_('The requested operation cannot be processed because of a programming error setting the `product_qty` field instead of the `product_uom_qty`.'))
|
||||
|
||||
@api.multi
|
||||
@api.constrains('product_uom_qty')
|
||||
def check_reserved_done_quantity(self):
|
||||
for move_line in self:
|
||||
if move_line.state == 'done' and not float_is_zero(move_line.product_uom_qty, precision_rounding=self.env['decimal.precision'].precision_get('Product Unit of Measure')):
|
||||
raise ValidationError(_('A done move line should never have a reserved quantity.'))
|
||||
|
||||
@api.multi
|
||||
@api.onchange('product_id', 'product_uom_id')
|
||||
def onchange_product_id(self):
|
||||
@@ -313,6 +318,8 @@ class StockMoveLine(models.Model):
|
||||
Quant._update_available_quantity(ml.product_id, ml.location_id, -taken_from_untracked_qty, lot_id=False, package_id=ml.package_id, owner_id=ml.owner_id)
|
||||
Quant._update_available_quantity(ml.product_id, ml.location_id, taken_from_untracked_qty, lot_id=ml.lot_id, package_id=ml.package_id, owner_id=ml.owner_id)
|
||||
Quant._update_available_quantity(ml.product_id, ml.location_dest_id, quantity, lot_id=ml.lot_id, package_id=ml.result_package_id, owner_id=ml.owner_id, in_date=in_date)
|
||||
# Reset the reserved quantity as we just moved it to the destination location.
|
||||
(self - ml_to_delete).with_context(bypass_reservation_update=True).write({'product_uom_qty': 0.00})
|
||||
|
||||
def _free_reservation(self, product_id, location_id, quantity, lot_id=None, package_id=None, owner_id=None):
|
||||
""" When editing a done move line or validating one with some forced quantities, it is
|
||||
|
||||
@@ -617,25 +617,28 @@ class Picking(models.Model):
|
||||
|
||||
# Check backorder should check for other barcodes
|
||||
if self.check_backorder():
|
||||
view = self.env.ref('stock.view_backorder_confirmation')
|
||||
wiz = self.env['stock.backorder.confirmation'].create({'pick_id': self.id})
|
||||
return {
|
||||
'name': _('Create Backorder?'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.backorder.confirmation',
|
||||
'views': [(view.id, 'form')],
|
||||
'view_id': view.id,
|
||||
'target': 'new',
|
||||
'res_id': wiz.id,
|
||||
'context': self.env.context,
|
||||
}
|
||||
return self.action_generate_backorder_wizard()
|
||||
self.action_done()
|
||||
return
|
||||
|
||||
do_new_transfer = button_validate #TODO: replace later
|
||||
|
||||
def action_generate_backorder_wizard(self):
|
||||
view = self.env.ref('stock.view_backorder_confirmation')
|
||||
wiz = self.env['stock.backorder.confirmation'].create({'pick_id': self.id})
|
||||
return {
|
||||
'name': _('Create Backorder?'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'stock.backorder.confirmation',
|
||||
'views': [(view.id, 'form')],
|
||||
'view_id': view.id,
|
||||
'target': 'new',
|
||||
'res_id': wiz.id,
|
||||
'context': self.env.context,
|
||||
}
|
||||
|
||||
def check_backorder(self):
|
||||
self.ensure_one()
|
||||
quantity_todo = {}
|
||||
|
||||
@@ -84,6 +84,7 @@ class MrpStockReport(models.TransientModel):
|
||||
('lot_id', '=', context.get('active_id')),
|
||||
('location_id.usage', '!=', 'internal'),
|
||||
('state', '=', 'done'),
|
||||
('move_id.returned_move_ids', '=', False),
|
||||
])
|
||||
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
|
||||
stream=stream, obj_ids=move_ids)
|
||||
@@ -99,6 +100,7 @@ class MrpStockReport(models.TransientModel):
|
||||
('lot_id', '=', context.get('active_id')),
|
||||
('location_dest_id.usage', '!=', 'internal'),
|
||||
('state', '=', 'done'),
|
||||
('move_id.returned_move_ids', '=', False),
|
||||
])
|
||||
res += self._lines(line_id, model_id=model_id, model='stock.move.line', level=level, parent_quant=parent_quant,
|
||||
stream=stream, obj_ids=move_ids)
|
||||
|
||||
@@ -28,6 +28,7 @@ class TestStockCommon(common.TransactionCase):
|
||||
self.supplier_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_suppliers')
|
||||
self.stock_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_stock')
|
||||
self.pack_location = self.ModelDataObj.xmlid_to_res_id('stock.location_pack_zone')
|
||||
self.output_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_output')
|
||||
self.customer_location = self.ModelDataObj.xmlid_to_res_id('stock.stock_location_customers')
|
||||
self.categ_unit = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_unit')
|
||||
self.categ_kgm = self.ModelDataObj.xmlid_to_res_id('product.product_uom_categ_kgm')
|
||||
|
||||
@@ -107,7 +107,7 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation
|
||||
|
||||
move1.action_done()
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation
|
||||
self.assertEqual(move_line.product_qty, 0) # change reservation to 0 for done move
|
||||
self.assertEqual(move1.state, 'done')
|
||||
|
||||
# no quants are created in the supplier location
|
||||
@@ -1607,7 +1607,7 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation when assgning a lot now
|
||||
|
||||
move1.action_done()
|
||||
self.assertEqual(move_line.product_qty, 5) # don't change reservation
|
||||
self.assertEqual(move_line.product_qty, 0) # change reservation to 0 for done move
|
||||
self.assertEqual(move1.state, 'done')
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 0.0)
|
||||
@@ -1671,8 +1671,8 @@ class StockMove(TransactionCase):
|
||||
self.assertEqual(untracked_move_line.product_qty, 3) # don't change reservation
|
||||
|
||||
move1.action_done()
|
||||
self.assertEqual(untracked_move_line.product_qty, 3) # don't change reservation
|
||||
self.assertEqual(tracked_move_line.product_qty, 2) # don't change reservation
|
||||
self.assertEqual(untracked_move_line.product_qty, 0) # change reservation to 0 for done move
|
||||
self.assertEqual(tracked_move_line.product_qty, 0) # change reservation to 0 for done move
|
||||
self.assertEqual(move1.state, 'done')
|
||||
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product3, self.stock_location), 0.0)
|
||||
@@ -1765,7 +1765,8 @@ class StockMove(TransactionCase):
|
||||
|
||||
move1.move_line_ids.lot_id = lot2.id
|
||||
|
||||
self.assertEqual(move1.reserved_availability, 1.0)
|
||||
# reserved_availability should always been 0 for done move.
|
||||
self.assertEqual(move1.reserved_availability, 0.0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location), 1.0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot1), 1.0)
|
||||
self.assertEqual(self.env['stock.quant']._get_available_quantity(self.product1, self.stock_location, lot_id=lot2), 0.0)
|
||||
@@ -2183,7 +2184,9 @@ class StockMove(TransactionCase):
|
||||
picking.action_assign()
|
||||
res_dict = picking.button_validate()
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.process()
|
||||
res_dict_for_back_order = wizard.process()
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
backorder_wizard.process()
|
||||
|
||||
self.assertEqual(len(picking.move_lines.move_line_ids), 1)
|
||||
self.assertEqual(picking.move_lines.quantity_done, 5.0)
|
||||
|
||||
@@ -45,6 +45,62 @@ class TestPickShip(TestStockCommon):
|
||||
})
|
||||
return picking_pick, picking_client
|
||||
|
||||
def create_pick_pack_ship(self):
|
||||
picking_ship = self.env['stock.picking'].create({
|
||||
'location_id': self.pack_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
})
|
||||
|
||||
ship = self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking_ship.id,
|
||||
'location_id': self.output_location,
|
||||
'location_dest_id': self.customer_location,
|
||||
})
|
||||
|
||||
picking_pack = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.pack_location,
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
})
|
||||
|
||||
pack = self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking_pack.id,
|
||||
'location_id': self.pack_location,
|
||||
'location_dest_id': self.output_location,
|
||||
'move_dest_ids': [(4, ship.id)],
|
||||
})
|
||||
|
||||
picking_pick = self.env['stock.picking'].create({
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.pack_location,
|
||||
'partner_id': self.partner_delta_id,
|
||||
'picking_type_id': self.picking_type_out,
|
||||
})
|
||||
|
||||
self.MoveObj.create({
|
||||
'name': self.productA.name,
|
||||
'product_id': self.productA.id,
|
||||
'product_uom_qty': 1,
|
||||
'product_uom': self.productA.uom_id.id,
|
||||
'picking_id': picking_pick.id,
|
||||
'location_id': self.stock_location,
|
||||
'location_dest_id': self.pack_location,
|
||||
'move_dest_ids': [(4, pack.id)],
|
||||
'state': 'confirmed',
|
||||
})
|
||||
return picking_pick, picking_pack, picking_ship
|
||||
|
||||
def test_mto_moves(self):
|
||||
"""
|
||||
10 in stock, do pick->ship and check ship is assigned when pick is done, then backorder of ship
|
||||
@@ -279,6 +335,178 @@ class TestPickShip(TestStockCommon):
|
||||
self.assertEqual(picking_client.state, 'assigned')
|
||||
self.assertEqual(picking_client.move_lines.reserved_availability, 5.0)
|
||||
|
||||
def test_pick_ship_return(self):
|
||||
""" Create pick and ship. Bring it ot the customer and then return
|
||||
it to stock. This test check the state and the quantity after each move in
|
||||
order to ensure that it is correct.
|
||||
"""
|
||||
picking_pick, picking_ship = self.create_pick_ship()
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
pack_location = self.env['stock.location'].browse(self.pack_location)
|
||||
customer_location = self.env['stock.location'].browse(self.customer_location)
|
||||
self.productA.tracking = 'lot'
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'product_id': self.productA.id,
|
||||
'name': '123456789'
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 10.0, lot_id=lot)
|
||||
|
||||
picking_pick.action_assign()
|
||||
picking_pick.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||
picking_pick.action_done()
|
||||
self.assertEqual(picking_pick.state, 'done')
|
||||
self.assertEqual(picking_ship.state, 'assigned')
|
||||
|
||||
picking_ship.action_assign()
|
||||
picking_ship.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||
picking_ship.action_done()
|
||||
|
||||
customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
|
||||
self.assertEqual(customer_quantity, 10, 'It should be one product in customer')
|
||||
|
||||
""" First we create the return picking for pick pinking.
|
||||
Since we do not have created the return between customer and
|
||||
output. This return should not be available and should only have
|
||||
picking pick as origin move.
|
||||
"""
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
|
||||
self.assertEqual(return_pick_picking.state, 'waiting')
|
||||
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 10.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
|
||||
self.assertEqual(return_ship_picking.state, 'assigned', 'Return ship picking should automatically be assigned')
|
||||
""" We created the return for ship picking. The origin/destination
|
||||
link between return moves should have been created during return creation.
|
||||
"""
|
||||
self.assertTrue(return_ship_picking.move_lines in return_pick_picking.move_lines.mapped('move_orig_ids'),
|
||||
'The pick return picking\'s moves should have the ship return picking\'s moves as origin')
|
||||
|
||||
self.assertTrue(return_pick_picking.move_lines in return_ship_picking.move_lines.mapped('move_dest_ids'),
|
||||
'The ship return picking\'s moves should have the pick return picking\'s moves as destination')
|
||||
|
||||
return_ship_picking.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 10.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_ship_picking.action_done()
|
||||
self.assertEqual(return_ship_picking.state, 'done')
|
||||
self.assertEqual(return_pick_picking.state, 'assigned')
|
||||
|
||||
customer_quantity = self.env['stock.quant']._get_available_quantity(self.productA, customer_location, lot_id=lot)
|
||||
self.assertEqual(customer_quantity, 0, 'It should be one product in customer')
|
||||
|
||||
pack_quantity = self.env['stock.quant']._get_available_quantity(self.productA, pack_location, lot_id=lot)
|
||||
self.assertEqual(pack_quantity, 0, 'It should be one product in pack location but is reserved')
|
||||
|
||||
# Should use previous move lot.
|
||||
return_pick_picking.move_lines[0].move_line_ids[0].qty_done = 10.0
|
||||
return_pick_picking.action_done()
|
||||
self.assertEqual(return_pick_picking.state, 'done')
|
||||
|
||||
stock_quantity = self.env['stock.quant']._get_available_quantity(self.productA, stock_location, lot_id=lot)
|
||||
self.assertEqual(stock_quantity, 10, 'The product is not back in stock')
|
||||
|
||||
def test_pick_pack_ship_return(self):
|
||||
""" This test do a pick pack ship delivery to customer and then
|
||||
return it to stock. Once everything is done, this test will check
|
||||
if all the link orgini/destination between moves are correct.
|
||||
"""
|
||||
picking_pick, picking_pack, picking_ship = self.create_pick_pack_ship()
|
||||
stock_location = self.env['stock.location'].browse(self.stock_location)
|
||||
self.productA.tracking = 'serial'
|
||||
lot = self.env['stock.production.lot'].create({
|
||||
'product_id': self.productA.id,
|
||||
'name': '123456789'
|
||||
})
|
||||
self.env['stock.quant']._update_available_quantity(self.productA, stock_location, 1.0, lot_id=lot)
|
||||
|
||||
picking_pick.action_assign()
|
||||
picking_pick.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||
picking_pick.action_done()
|
||||
|
||||
picking_pack.action_assign()
|
||||
picking_pack.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||
picking_pack.action_done()
|
||||
|
||||
picking_ship.action_assign()
|
||||
picking_ship.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||
picking_ship.action_done()
|
||||
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_ship.ids, active_id=picking_ship.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 1.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_ship_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
|
||||
return_ship_picking.move_lines[0].move_line_ids[0].write({
|
||||
'qty_done': 1.0,
|
||||
'lot_id': lot.id,
|
||||
})
|
||||
return_ship_picking.action_done()
|
||||
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_pack.ids, active_id=picking_pack.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 1.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_pack_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
|
||||
return_pack_picking.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||
return_pack_picking.action_done()
|
||||
|
||||
stock_return_picking = self.env['stock.return.picking']\
|
||||
.with_context(active_ids=picking_pick.ids, active_id=picking_pick.ids[0])\
|
||||
.create({})
|
||||
stock_return_picking.product_return_moves.quantity = 1.0
|
||||
stock_return_picking_action = stock_return_picking.create_returns()
|
||||
return_pick_picking = self.env['stock.picking'].browse(stock_return_picking_action['res_id'])
|
||||
|
||||
return_pick_picking.move_lines[0].move_line_ids[0].qty_done = 1.0
|
||||
return_pick_picking.action_done()
|
||||
|
||||
# Now that everything is returned we will check if the return moves are correctly linked between them.
|
||||
# +--------------------------------------------------------------------------------------------------------+
|
||||
# | -- picking_pick(1) --> -- picking_pack(2) --> -- picking_ship(3) -->
|
||||
# | Stock Pack Output Customer
|
||||
# | <--- return pick(6) -- <--- return pack(5) -- <--- return ship(4) --
|
||||
# +--------------------------------------------------------------------------------------------------------+
|
||||
# Recaps of final link (MO = move_orig_ids, MD = move_dest_ids)
|
||||
# picking_pick(1) : MO = (), MD = (2,6)
|
||||
# picking_pack(2) : MO = (1), MD = (3,5)
|
||||
# picking ship(3) : MO = (2), MD = (4)
|
||||
# return ship(4) : MO = (3), MD = (5)
|
||||
# return pack(5) : MO = (2, 4), MD = (6)
|
||||
# return pick(6) : MO = (1, 5), MD = ()
|
||||
|
||||
self.assertEqual(len(picking_pick.move_lines.move_orig_ids), 0, 'Picking pick should not have origin moves')
|
||||
self.assertEqual(set(picking_pick.move_lines.move_dest_ids.ids), set((picking_pack.move_lines | return_pick_picking.move_lines).ids))
|
||||
|
||||
self.assertEqual(set(picking_pack.move_lines.move_orig_ids.ids), set(picking_pick.move_lines.ids))
|
||||
self.assertEqual(set(picking_pack.move_lines.move_dest_ids.ids), set((picking_ship.move_lines | return_pack_picking.move_lines).ids))
|
||||
|
||||
self.assertEqual(set(picking_ship.move_lines.move_orig_ids.ids), set(picking_pack.move_lines.ids))
|
||||
self.assertEqual(set(picking_ship.move_lines.move_dest_ids.ids), set(return_ship_picking.move_lines.ids))
|
||||
|
||||
self.assertEqual(set(return_ship_picking.move_lines.move_orig_ids.ids), set(picking_ship.move_lines.ids))
|
||||
self.assertEqual(set(return_ship_picking.move_lines.move_dest_ids.ids), set(return_pack_picking.move_lines.ids))
|
||||
|
||||
self.assertEqual(set(return_pack_picking.move_lines.move_orig_ids.ids), set((picking_pack.move_lines | return_ship_picking.move_lines).ids))
|
||||
self.assertEqual(set(return_pack_picking.move_lines.move_dest_ids.ids), set(return_pick_picking.move_lines.ids))
|
||||
|
||||
self.assertEqual(set(return_pick_picking.move_lines.move_orig_ids.ids), set((picking_pick.move_lines | return_pack_picking.move_lines).ids))
|
||||
self.assertEqual(len(return_pick_picking.move_lines.move_dest_ids), 0)
|
||||
|
||||
class TestSinglePicking(TestStockCommon):
|
||||
def test_backorder_1(self):
|
||||
@@ -400,8 +628,8 @@ class TestSinglePicking(TestStockCommon):
|
||||
|
||||
# self.assertEqual(move1.product_qty, 1.0)
|
||||
# self.assertEqual(move1.quantity_done, 1.0)
|
||||
self.assertEqual(move1.reserved_availability, 1.0)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 1.0) # should keep the reservation
|
||||
self.assertEqual(move1.reserved_availability, 0.0)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 0.0) # change reservation to 0 for done move
|
||||
self.assertEqual(move1.move_line_ids.qty_done, 1.0)
|
||||
self.assertEqual(move1.state, 'done')
|
||||
|
||||
@@ -455,8 +683,8 @@ class TestSinglePicking(TestStockCommon):
|
||||
|
||||
self.assertEqual(move1.product_qty, 1.0)
|
||||
self.assertEqual(move1.quantity_done, 1.0)
|
||||
self.assertEqual(move1.reserved_availability, 1.0)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 1.0) # should keep the reservation
|
||||
self.assertEqual(move1.reserved_availability, 0.0)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 0.0) # change reservation to 0 for done move
|
||||
self.assertEqual(move1.move_line_ids.qty_done, 1.0)
|
||||
self.assertEqual(move1.state, 'done')
|
||||
|
||||
@@ -503,8 +731,8 @@ class TestSinglePicking(TestStockCommon):
|
||||
|
||||
self.assertEqual(move1.product_qty, 1.0)
|
||||
self.assertEqual(move1.quantity_done, 1.0)
|
||||
self.assertEqual(move1.reserved_availability, 1.0)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 1.0) # should keep the reservation
|
||||
self.assertEqual(move1.reserved_availability, 0.0)
|
||||
self.assertEqual(move1.move_line_ids.product_qty, 0.0) # change reservation to 0 for done move
|
||||
self.assertEqual(move1.move_line_ids.qty_done, 1.0)
|
||||
self.assertEqual(move1.state, 'done')
|
||||
|
||||
|
||||
@@ -649,7 +649,9 @@ class TestStockFlow(TestStockCommon):
|
||||
|
||||
res_dict = picking_in_B.button_validate()
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.process()
|
||||
res_dict_for_back_order = wizard.process()
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
backorder_wizard.process()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Check incoming shipment
|
||||
@@ -846,7 +848,9 @@ class TestStockFlow(TestStockCommon):
|
||||
|
||||
res_dict = picking_out.button_validate()
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.process()
|
||||
res_dict_for_back_order = wizard.process()
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
backorder_wizard.process()
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Check product stock location quantity and quantity available.
|
||||
@@ -927,7 +931,9 @@ class TestStockFlow(TestStockCommon):
|
||||
# Transfer Incoming shipment.
|
||||
res_dict = picking_in.button_validate()
|
||||
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
|
||||
wizard.process()
|
||||
res_dict_for_back_order = wizard.process()
|
||||
backorder_wizard = self.env[(res_dict_for_back_order.get('res_model'))].browse(res_dict_for_back_order.get('res_id'))
|
||||
backorder_wizard.process()
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Check incoming shipment after transfer.
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
<field name="package_id" invisible="not context.get('show_package')" groups="stock.group_tracking_lot"/>
|
||||
<field name="result_package_id" groups="stock.group_tracking_lot"/>
|
||||
<field name="owner_id" groups="stock.group_tracking_owner"/>
|
||||
<field name="product_uom_qty" readonly="1"/>
|
||||
<field name="product_uom_qty" invisible="not context.get('show_reserved_quantity')" readonly="1"/>
|
||||
<field name="qty_done"/>
|
||||
<field name="product_uom_id" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': [('product_uom_qty', '!=', 0.0)]}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
</tree>
|
||||
|
||||
@@ -277,7 +277,7 @@
|
||||
<page string="Operations">
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="move_lines" attrs="{'readonly': [('state', 'in', ('cancel', 'done'))]}" context="{'default_picking_id': id, 'form_view_ref':'stock.view_move_picking_form', 'address_in_id': partner_id, 'default_picking_type_id': picking_type_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id}">
|
||||
<tree decoration-danger="quantity_done > reserved_availability" decoration-muted="scrapped == True" string="Stock Moves" editable="bottom">
|
||||
<tree decoration-danger="state != 'done' and quantity_done > reserved_availability" decoration-muted="scrapped == True" string="Stock Moves" editable="bottom">
|
||||
<field name="name" invisible="1"/>
|
||||
<field name="date_expected" invisible="1"/>
|
||||
<field name="state" invisible="1" readonly="0"/>
|
||||
@@ -296,7 +296,7 @@
|
||||
<field name="product_uom_qty" string="Initial Demand" attrs="{'readonly': [('is_initial_demand_editable', '=', False), ('state', '!=', 'draft')]}"/>
|
||||
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="product.group_uom"/>
|
||||
<field name="reserved_availability" string="Reserved"/>
|
||||
<field name="quantity_done" string="Done" attrs="{'readonly': [('show_details_visible', '=', True)], 'invisible': ['|', ('state', 'in', ['draft', 'confirmed', 'waiting']), ('show_operations', '=', True)]}"/>
|
||||
<field name="quantity_done" string="Done" attrs="{'readonly': ['|', ('show_details_visible', '=', True), ('state', '=', 'cancel')], 'invisible': ['|', ('state', 'in', ['draft', 'confirmed', 'waiting']), ('show_operations', '=', True)]}"/>
|
||||
<button name="action_show_details" string="Register lots, packs, location" type="object" icon="fa-list" attrs="{'invisible': [('show_details_visible', '=', False)]}"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -34,4 +34,6 @@ class StockImmediateTransfer(models.TransientModel):
|
||||
move_line.qty_done = move_line.product_uom_qty
|
||||
else:
|
||||
move.quantity_done = move.product_uom_qty
|
||||
if self.pick_id.check_backorder():
|
||||
return self.pick_id.action_generate_backorder_wizard()
|
||||
self.pick_id.do_transfer()
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
<form string="Immediate transfer?">
|
||||
<group>
|
||||
<p>
|
||||
You haven't set processed (<i>done</i>) quantities. Click <i>apply</i> and
|
||||
Odoo will process all quantities to do.
|
||||
You haven't entered (<i>done</i>) quantities, by clicking on <i>apply</i>
|
||||
Odoo will process all the reserved quantities.
|
||||
</p>
|
||||
</group>
|
||||
<footer>
|
||||
|
||||
@@ -110,8 +110,19 @@ class ReturnPicking(models.TransientModel):
|
||||
returned_lines += 1
|
||||
vals = self._prepare_move_default_values(return_line, new_picking)
|
||||
r = return_line.move_id.copy(vals)
|
||||
r.write({'move_orig_ids': [(4, return_line.move_id.id, False)]})
|
||||
vals = {}
|
||||
|
||||
# +--------------------------------------------------------------------------------------------------------+
|
||||
# | picking_pick <--Move Orig-- picking_pack --Move Dest--> picking_ship
|
||||
# | | returned_move_ids ↑ | returned_move_ids
|
||||
# | ↓ | return_line.move_id ↓
|
||||
# | return pick(Add as dest) return toLink return ship(Add as orig)
|
||||
# +--------------------------------------------------------------------------------------------------------+
|
||||
move_orig_to_link = return_line.move_id.move_dest_ids.mapped('returned_move_ids')
|
||||
move_dest_to_link = return_line.move_id.move_orig_ids.mapped('returned_move_ids')
|
||||
vals['move_orig_ids'] = [(4, m.id) for m in move_orig_to_link | return_line.move_id]
|
||||
vals['move_dest_ids'] = [(4, m.id) for m in move_dest_to_link]
|
||||
r.write(vals)
|
||||
if not returned_lines:
|
||||
raise UserError(_("Please specify at least one non-zero quantity."))
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="header_footer_custom_payment" inherit_id="website.footer_default" customize_show="True" active="False" name="Payment Acquirers">
|
||||
<template id="header_footer_custom_payment" inherit_id="website.footer_default" customize_show="True" active="False" name="Payment Methods">
|
||||
<xpath expr="//div[@id='info']" position="inside">
|
||||
<h4 class="payment_method_title">Payment Acquirer</h4>
|
||||
<h4 class="payment_method_title">Payment Methods</h4>
|
||||
<ul class="payment_method_list list-inline">
|
||||
<t t-foreach="request.website.payment_acquirers()" t-as="acquirer">
|
||||
<li>
|
||||
@@ -12,4 +12,4 @@
|
||||
</ul>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
</odoo>
|
||||
|
||||
@@ -1573,7 +1573,10 @@
|
||||
|
||||
<template id="payment_confirmation_status">
|
||||
<div class="oe_website_sale_tx_status" t-att-data-order-id="order.id">
|
||||
<div t-att-class="' panel ' + (order.payment_tx_id.state == 'pending' and 'panel-info') or (order.payment_tx_id.state == 'done' and 'panel-success') or 'panel-danger'">
|
||||
<div t-att-class="'panel %s' % (
|
||||
(order.payment_tx_id.state == 'pending' and 'panel-info') or
|
||||
(order.payment_tx_id.state == 'done' and 'panel-success') or
|
||||
'panel-danger')">
|
||||
<a groups="base.group_system" class="btn btn-primary pull-right" target="_blank"
|
||||
t-att-href="'/web#model=%s&id=%s&action=%s&view_type=form' % ('payment.acquirer', order.payment_acquirer_id.id, 'payment.action_payment_acquirer')">
|
||||
<i class="fa fa-cog"></i>
|
||||
|
||||
Reference in New Issue
Block a user