[MERGE] forward port of branch saas-17 up to b970bf865a

This commit is contained in:
Denis Ledoux
2017-08-18 10:44:22 +02:00
38 changed files with 956 additions and 340 deletions
+2 -1
View File
@@ -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
+1 -30
View File
@@ -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
+15 -4
View File
@@ -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>&nbsp;</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>&nbsp;</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;
}
}
+3 -1
View File
@@ -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)&gt;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>
+57 -45
View File
@@ -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)&gt;1 and len(pms)==0 and acquirers[0]!=acq) else 'hidden' if len(pms)&gt;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>
+25 -1
View File
@@ -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.
+31 -8
View File
@@ -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="&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;"/>
<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>
+34
View File
@@ -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 = ''
+6 -4
View File
@@ -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="&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;"/>
<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>
+25 -1
View File
@@ -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
+44 -3
View File
@@ -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="&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;&#8226;&#8226;&#8226;&#8226;&#32;"/>
<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">
+1 -7
View File
@@ -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.')
+2 -2
View File
@@ -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)
+1
View File
@@ -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'
),
}
+13 -6
View File
@@ -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
+17 -14
View File
@@ -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)
+1
View File
@@ -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')
+9 -6
View File
@@ -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)
+234 -6
View File
@@ -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')
+9 -3
View File
@@ -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.
+1 -1
View File
@@ -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>
+2 -2
View File
@@ -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>
+12 -1
View File
@@ -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>
+4 -1
View File
@@ -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&amp;id=%s&amp;action=%s&amp;view_type=form' % ('payment.acquirer', order.payment_acquirer_id.id, 'payment.action_payment_acquirer')">
<i class="fa fa-cog"></i>