diff --git a/addons/payment/__manifest__.py b/addons/payment/__manifest__.py index 545c528f610..d63ded73884 100644 --- a/addons/payment/__manifest__.py +++ b/addons/payment/__manifest__.py @@ -13,6 +13,7 @@ 'views/payment_views.xml', 'views/account_payment_views.xml', 'views/payment_templates.xml', + 'views/payment_assets.xml', 'views/res_partner_views.xml', 'security/ir.model.access.csv', 'security/payment_security.xml', diff --git a/addons/payment/static/src/js/payment_transaction.js b/addons/payment/static/src/js/payment_transaction.js new file mode 100644 index 00000000000..eb945f7a220 --- /dev/null +++ b/addons/payment/static/src/js/payment_transaction.js @@ -0,0 +1,81 @@ +odoo.define('payment.transaction', function (require) { +"use strict"; + +var ajax = require('web.ajax'); + +$(document).ready(function () { + // When choosing an acquirer, display its Pay Now button + var $payment = $("#payment_method"); + $payment.on("click", "input[name='acquirer'], a.btn_payment_token", function (ev) { + var ico_off = 'fa-circle-o'; + var ico_on = 'fa-dot-circle-o'; + + var payment_id = $(ev.currentTarget).val() || $(this).data('acquirer'); + var token = $(ev.currentTarget).data('token') || ''; + + $("div.o_payment_acquirer_button[data-id='"+payment_id+"']", $payment).attr('data-token', token); + $("div.js_payment a.list-group-item").removeClass("list-group-item-info"); + $('span.js_radio').switchClass(ico_on, ico_off, 0); + if (token) { + $("div.o_payment_acquirer_button div.token_hide").hide(); + $(ev.currentTarget).find('span.js_radio').switchClass(ico_off, ico_on, 0); + $(ev.currentTarget).parents('li').find('input').prop("checked", true); + $(ev.currentTarget).addClass("list-group-item-info"); + } + else{ + $("div.o_payment_acquirer_button div.token_hide").show(); + } + $("div.o_payment_acquirer_button[data-id]", $payment).addClass("hidden"); + $("div.o_payment_acquirer_button[data-id='"+payment_id+"']", $payment).removeClass("hidden"); + + }) + .find("input[name='acquirer']:checked").click(); + + // When clicking on payment button: create the tx using json then continue to the acquirer + $payment.on("click", 'button[type="submit"], button[name="submit"]', function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var $form = $(ev.currentTarget).parents('form'); + var acquirer = $(ev.currentTarget).parents('div.o_payment_acquirer_button').first(); + var tx_data = acquirer.data(); + if (! tx_data.id || ! tx_data.callUrl) { + return false; + } + + var params = {"tx_type": tx_data.txType || acquirer.find('input[name="odoo_save_token"]').is(':checked') ? 'form_save': 'form'}; + var acquirer_token = acquirer.attr('data-token'); // !=data + if (acquirer_token) { + params.token = acquirer_token; + } + if (tx_data.access_token) { + params.access_token = tx_data.access_token; + } + $form.off('submit'); + ajax.jsonRpc(tx_data.callUrl, 'call', params).then(function (data) { + $(data).appendTo('body').submit(); + }); + return false; + }); + + $('div.oe_pay_token').on('click', 'a.js_btn_valid_tx', function() { + $('div.js_token_load').toggle(); + + var $form = $(this).parents('form'); + ajax.jsonRpc($form.attr('action'), 'call', $.deparam($form.serialize())).then(function (data) { + if (data.url) { + window.location = data.url; + } + else { + $('div.js_token_load').toggle(); + if (!data.success && data.error) { + $('div.oe_pay_token div.panel-body p').html(data.error + "

" + _('Retry ? ')); + $('div.oe_pay_token div.panel-body').parents('div').removeClass('panel-info').addClass('panel-danger'); + } + } + }); + + }); + +}); + +}); diff --git a/addons/payment/views/payment_assets.xml b/addons/payment/views/payment_assets.xml new file mode 100644 index 00000000000..47d4717ef97 --- /dev/null +++ b/addons/payment/views/payment_assets.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/addons/payment_stripe/static/src/js/stripe.js b/addons/payment_stripe/static/src/js/stripe.js index 7338313e245..159bdc14214 100644 --- a/addons/payment_stripe/static/src/js/stripe.js +++ b/addons/payment_stripe/static/src/js/stripe.js @@ -36,13 +36,13 @@ odoo.define('payment_stripe.stripe', function(require) { $(this).attr('disabled','disabled'); var $form = $(e.currentTarget).parents('form'); - var acquirer_id = $(e.currentTarget).closest('div.oe_sale_acquirer_button,div.oe_quote_acquirer_button,div.o_website_payment_new_payment'); + var acquirer_id = $(e.currentTarget).closest('div.o_payment_acquirer_button,div.oe_quote_acquirer_button,div.o_website_payment_new_payment'); acquirer_id = acquirer_id.data('id') || acquirer_id.data('acquirer_id'); if (! acquirer_id) { return false; } - var so_token = $("input[name='token']").val(); + var access_token = $("input[name='token']").val(); var so_id = $("input[name='return_url']").val().match(/quote\/([0-9]+)/) || undefined; if (so_id) { so_id = parseInt(so_id[1]); @@ -65,7 +65,7 @@ odoo.define('payment_stripe.stripe', function(require) { } else { ajax.jsonRpc('/shop/payment/transaction/' + acquirer_id, 'call', { so_id: so_id, - so_token: so_token + access_token: access_token }, {'async': false}).then(function (data) { $form.html(data); handler.open({ diff --git a/addons/sale/models/sale.py b/addons/sale/models/sale.py index b3e8af8dc6e..a7c5954eef7 100644 --- a/addons/sale/models/sale.py +++ b/addons/sale/models/sale.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. +import uuid + from itertools import groupby from datetime import datetime, timedelta @@ -88,6 +90,9 @@ class SaleOrder(models.Model): return '

%s

' % (help) return super(SaleOrder, self).get_empty_list_help(help) + def _get_default_access_token(self): + return str(uuid.uuid4()) + @api.model def _default_note(self): return self.env.user.company_id.sale_note @@ -110,6 +115,9 @@ class SaleOrder(models.Model): name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, states={'draft': [('readonly', False)]}, index=True, default=lambda self: _('New')) origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.") client_order_ref = fields.Char(string='Customer Reference', copy=False) + access_token = fields.Char( + 'Security Token', copy=False, + default=_get_default_access_token) state = fields.Selection([ ('draft', 'Quotation'), @@ -279,6 +287,20 @@ class SaleOrder(models.Model): default['order_line'] = [(0, 0, line.copy_data()[0]) for line in self.order_line.filtered(lambda l: not l.is_downpayment)] return super(SaleOrder, self).copy_data(default) + @api.model_cr_context + def _init_column(self, column_name): + """ Initialize the value of the given column for existing rows. + Overridden here because we skip generating unique access tokens + for potentially tons of existing sale orders, should they be needed, + they will be generated on the fly. + """ + if column_name != 'access_token': + super(SaleOrder, self)._init_column(column_name) + + def _generate_access_token(self): + for order in self: + order.access_token = self._get_default_access_token() + @api.multi def _prepare_invoice(self): """ diff --git a/addons/sale_payment/__init__.py b/addons/sale_payment/__init__.py new file mode 100644 index 00000000000..e9917144f69 --- /dev/null +++ b/addons/sale_payment/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import models \ No newline at end of file diff --git a/addons/sale_payment/__manifest__.py b/addons/sale_payment/__manifest__.py new file mode 100644 index 00000000000..6c1caaef606 --- /dev/null +++ b/addons/sale_payment/__manifest__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Payment - Sale', + 'category': 'Sales', + 'summary': 'Link Sales and Payment', + 'version': '1.0', + 'description': """Link Sales and Payment + +Provide tools for sale-related payment + + * specific management of transactions like sale order confirmation + * tools method to handle transactions when allowing to pay / sell + * JS code to handle a payment form (eCommerce + * add payment-related fields on SO +""", + 'depends': ['payment', 'sale'], + 'data': [ + 'views/payment_views.xml', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/addons/sale_payment/models/__init__.py b/addons/sale_payment/models/__init__.py new file mode 100644 index 00000000000..51af63e66bb --- /dev/null +++ b/addons/sale_payment/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import payment +from . import sale_order \ No newline at end of file diff --git a/addons/sale_payment/models/payment.py b/addons/sale_payment/models/payment.py new file mode 100644 index 00000000000..8553672a71e --- /dev/null +++ b/addons/sale_payment/models/payment.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import logging + +from odoo import api, fields, models, _ +from odoo.tools import float_compare + +_logger = logging.getLogger(__name__) + + +class PaymentTransaction(models.Model): + _inherit = 'payment.transaction' + + # link with the sales order + # YTI FIXME: The auto_join seems useless + sale_order_id = fields.Many2one('sale.order', string='Sales Order', auto_join=True) + + # -------------------------------------------------- + # Sale management + # -------------------------------------------------- + + @api.model + def form_feedback(self, data, acquirer_name): + """ Override to confirm the sales order, if defined, and if the transaction + is done. """ + tx = None + res = super(PaymentTransaction, self).form_feedback(data, acquirer_name) + + # fetch the tx + tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name + if hasattr(self, tx_find_method_name): + tx = getattr(self, tx_find_method_name)(data) + _logger.info('<%s> transaction processed: tx ref:%s, tx amount: %s', acquirer_name, tx.reference if tx else 'n/a', tx.amount if tx else 'n/a') + + if tx: + # Auto-confirm SO if necessary + tx._confirm_so() + + return res + + def _confirm_so(self): + for tx in self: + # check tx state, confirm the potential SO + if not tx.sale_order_id or tx.sale_order_id.state not in ['draft', 'sent']: + # _logger.warning('<%s> transaction incorrect sale order %s (ID %s, state %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id, tx.sale_order_id.state) + continue + if not float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0: + _logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id) + continue + try: + if tx.state == 'authorized' and tx.acquirer_id.capture_manually: + _logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id) + tx.sale_order_id.with_context(send_email=True).action_confirm() + + if tx.state == 'done': + _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id) + tx.sale_order_id.with_context(send_email=True).action_confirm() + tx._generate_and_pay_invoice() + elif tx.state not in ['cancel', 'error'] and tx.sale_order_id.state == 'draft': + _logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id) + tx.sale_order_id.force_quotation_send() + else: + _logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', tx.acquirer_id.provider, tx.sale_order_id.name, tx.sale_order_id.id) + except Exception: + _logger.exception('Fail to confirm the order or send the confirmation email%s', tx and ' for the transaction %s' % tx.reference or '') + + def _generate_and_pay_invoice(self): + self.sale_order_id._force_lines_to_invoice_policy_order() + + # force company to ensure journals/accounts etc. are correct + # company_id needed for default_get on account.journal + # force_company needed for company_dependent fields + ctx_company = {'company_id': self.sale_order_id.company_id.id, + 'force_company': self.sale_order_id.company_id.id} + created_invoice = self.sale_order_id.with_context(**ctx_company).action_invoice_create() + created_invoice = self.env['account.invoice'].browse(created_invoice).with_context(**ctx_company) + + if created_invoice: + _logger.info('<%s> transaction completed, auto-generated invoice %s (ID %s) for %s (ID %s)', + self.acquirer_id.provider, created_invoice.name, created_invoice.id, self.sale_order_id.name, self.sale_order_id.id) + + created_invoice.action_invoice_open() + if not self.acquirer_id.journal_id: + default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1) + if not default_journal: + _logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)', + self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id) + self.acquirer_id.journal_id = default_journal + created_invoice.pay_and_reconcile(self.acquirer_id.journal_id, pay_amount=created_invoice.amount_total) + if created_invoice.payment_ids: + created_invoice.payment_ids[0].payment_transaction_id = self + else: + _logger.warning('<%s> transaction completed, could not auto-generate invoice for %s (ID %s)', + self.acquirer_id.provider, self.sale_order_id.name, self.sale_order_id.id) + + # -------------------------------------------------- + # Tools for payment + # -------------------------------------------------- + + def confirm_sale_token(self): + """ Confirm a transaction token and call SO confirmation if it is a success. + + :return: True if success; error string otherwise """ + self.ensure_one() + if self.payment_token_id and self.partner_id == self.sale_order_id.partner_id: + try: + s2s_result = self.s2s_do_transaction() + except Exception as e: + _logger.warning(_("Payment transaction (%s) failed : <%s>") % (self.id, str(e))) + return _("Payment transaction failed (Contact Administrator)") + + valid_state = 'authorized' if self.acquirer_id.capture_manually else 'done' + if not s2s_result or self.state != valid_state: + return _("Payment transaction failed (%s)" % self.state_message) + try: + # Auto-confirm SO if necessary + self._confirm_so() + return True + except Exception as e: + _logger.warning(_("Payment transaction (%s) failed : <%s>") % (self.id, str(e))) + return _("Payment transaction / SO Confirmation failed (Contact Administrator)") + return _('Tx missmatch') + + def check_or_create_sale_tx(self, order, acquirer, payment_token=None, tx_type='form', add_tx_values=None, reset_draft=True): + tx = self + # incorrect state or unexisting tx + if not self or self.state in ['error', 'cancel']: + tx = False + # unmatching + if (self and acquirer and self.acquirer_id != acquirer) or (self and self.sale_order_id != order): + tx = False + # new or distinct token + if payment_token and tx.payment_token_id and payment_token != self.payment_token_id: + tx = False + + # still draft tx, no more info -> rewrite on tx or create a new one depending on parameter + if tx and tx.state == 'draft': + if reset_draft: + tx.write(dict( + self.on_change_partner_id(order.partner_id.id).get('value', {}), + amount=order.amount_total, + type=tx_type) + ) + else: + tx = False + + if not tx: + tx_values = { + 'acquirer_id': acquirer.id, + 'type': tx_type, + 'amount': order.amount_total, + 'currency_id': order.pricelist_id.currency_id.id, + 'partner_id': order.partner_id.id, + 'partner_country_id': order.partner_id.country_id.id, + 'reference': self.get_next_reference(order.name), + 'sale_order_id': order.id, + } + if add_tx_values: + tx_values.update(add_tx_values) + if payment_token and payment_token.sudo().partner_id == order.partner_id: + tx_values['payment_token_id'] = payment_token.id + + tx = self.create(tx_values) + + # update quotation + order.write({ + 'payment_tx_id': tx.id, + }) + + return tx + + def render_sale_button(self, order, return_url, submit_txt=None, render_values=None): + values = { + 'return_url': return_url, + 'partner_id': order.partner_shipping_id.id or order.partner_invoice_id.id, + 'billing_partner_id': order.partner_invoice_id.id, + } + if render_values: + values.update(render_values) + return self.acquirer_id.with_context(submit_class='btn btn-primary', submit_txt=submit_txt or _('Pay Now')).sudo().render( + self.reference, + order.amount_total, + order.pricelist_id.currency_id.id, + values=values, + ) diff --git a/addons/sale_payment/models/sale_order.py b/addons/sale_payment/models/sale_order.py new file mode 100644 index 00000000000..9c590529302 --- /dev/null +++ b/addons/sale_payment/models/sale_order.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + payment_tx_id = fields.Many2one('payment.transaction', string='Last Transaction', copy=False) + payment_acquirer_id = fields.Many2one('payment.acquirer', string='Payment Acquirer', related='payment_tx_id.acquirer_id', store=True) diff --git a/addons/sale_payment/views/payment_views.xml b/addons/sale_payment/views/payment_views.xml new file mode 100644 index 00000000000..939089fbf74 --- /dev/null +++ b/addons/sale_payment/views/payment_views.xml @@ -0,0 +1,18 @@ + + + + Pending Payment Transactions + payment.transaction + form + tree,form + [('sale_order_id.team_id', '=', active_id), ('state', '=', 'pending')] + + + + Authorized Payment Transactions + payment.transaction + form + tree,form + [('sale_order_id.team_id', '=', active_id), ('state', '=', 'authorized')] + + diff --git a/addons/website_account/__init__.py b/addons/website_account/__init__.py new file mode 100644 index 00000000000..7d34c7c054a --- /dev/null +++ b/addons/website_account/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import controllers +from . import models diff --git a/addons/website_account/__manifest__.py b/addons/website_account/__manifest__.py new file mode 100644 index 00000000000..ad4d55c2f45 --- /dev/null +++ b/addons/website_account/__manifest__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +{ + 'name': 'Account Customer Portal', + 'category': 'Website', + 'summary': 'Add your accounting document in the frontend portal', + 'version': '1.0', + 'description': """ +Add your accounting document in the frontend portal. Give customers the list and the state of their invoices. + """, + 'depends': [ + 'account', + 'website_portal', + 'website_payment', + ], + 'data': [ + 'views/account_views.xml', + 'views/account_templates.xml', + 'security/ir.model.access.csv', + 'security/account.xml', + ], + 'demo': [], + 'installable': True, +} diff --git a/addons/website_account/controllers/__init__.py b/addons/website_account/controllers/__init__.py new file mode 100644 index 00000000000..67dc7006e4c --- /dev/null +++ b/addons/website_account/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import portal_account diff --git a/addons/website_account/controllers/portal_account.py b/addons/website_account/controllers/portal_account.py new file mode 100644 index 00000000000..9ae2e743003 --- /dev/null +++ b/addons/website_account/controllers/portal_account.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import http, _ +from odoo.exceptions import AccessError +from odoo.http import request + +from odoo.addons.website_portal.controllers.main import website_account + + +class PortalAccount(website_account): + + def _prepare_portal_layout_values(self): + values = super(PortalAccount, self)._prepare_portal_layout_values() + partner = request.env.user.partner_id + + invoice_count = request.env['account.invoice'].search_count([ + ('type', 'in', ['out_invoice', 'out_refund']), + ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), + ('state', 'in', ['open', 'paid', 'cancel']) + ]) + + values['invoice_count'] = invoice_count + return values + + @http.route(['/my/invoices', '/my/invoices/page/'], type='http', auth="user", website=True) + def portal_my_invoices(self, page=1, date_begin=None, date_end=None, sortby=None, **kw): + values = self._prepare_portal_layout_values() + partner = request.env.user.partner_id + AccountInvoice = request.env['account.invoice'] + + domain = [ + ('type', 'in', ['out_invoice', 'out_refund']), + ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), + ('state', 'in', ['open', 'paid', 'cancelled']) + ] + + searchbar_sortings = { + 'date': {'label': _('Invoice Date'), 'order': 'date_invoice desc'}, + 'duedate': {'label': _('Due Date'), 'order': 'date_due desc'}, + 'name': {'label': _('Reference'), 'order': 'name desc'}, + 'state': {'label': _('Status'), 'order': 'state'}, + } + # default sort by order + if not sortby: + sortby = 'date' + order = searchbar_sortings[sortby]['order'] + + archive_groups = self._get_archive_groups('account.invoice', domain) + if date_begin and date_end: + domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)] + + # count for pager + invoice_count = AccountInvoice.search_count(domain) + # pager + pager = request.website.pager( + url="/my/invoices", + url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby}, + total=invoice_count, + page=page, + step=self._items_per_page + ) + # content according to pager and archive selected + invoices = AccountInvoice.search(domain, order=order, limit=self._items_per_page, offset=pager['offset']) + values.update({ + 'date': date_begin, + 'invoices': invoices, + 'page_name': 'invoice', + 'pager': pager, + 'archive_groups': archive_groups, + 'default_url': '/my/invoices', + 'searchbar_sortings': searchbar_sortings, + 'sortby': sortby, + }) + return request.render("website_account.portal_my_invoices", values) + + @http.route(['/my/invoices/'], type='http', auth="user", website=True) + def portal_my_invoices_report(self, invoice_id, **kw): + invoice = request.env['account.invoice'].browse(invoice_id) + try: + invoice.check_access_rights('read') + invoice.check_access_rule('read') + except AccessError: + return request.render("website.403") + # print report as sudo, since it require access to taxes, payment term, ... and portal + # does not have those access rights. + pdf = request.env.ref('account.account_invoices').sudo().render_qweb_pdf([invoice_id])[0] + pdfhttpheaders = [ + ('Content-Type', 'application/pdf'), + ('Content-Length', len(pdf)), + ] + return request.make_response(pdf, headers=pdfhttpheaders) + + def details_form_validate(self, data): + error, error_message = super(website_account, self).details_form_validate(data) + # prevent VAT/name change if invoices exist + partner = request.env['res.users'].browse(request.uid).partner_id + invoices = request.env['account.invoice'].sudo().search_count([('partner_id', '=', partner.id), ('state', 'not in', ['draft', 'cancel'])]) + if invoices: + if 'vat' in partner and (data['vat'] or False) != (partner.vat or False): + error['vat'] = 'error' + error_message.append(_('Changing VAT number is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) + if 'name' in data and (data['name'] or False) != (partner.name or False): + error['name'] = 'error' + error_message.append(_('Changing your name is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) + return error, error_message diff --git a/addons/website_account/models/__init__.py b/addons/website_account/models/__init__.py new file mode 100644 index 00000000000..0967df7c738 --- /dev/null +++ b/addons/website_account/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import account_invoice diff --git a/addons/website_portal_sale/models/account_invoice.py b/addons/website_account/models/account_invoice.py similarity index 100% rename from addons/website_portal_sale/models/account_invoice.py rename to addons/website_account/models/account_invoice.py diff --git a/addons/website_account/security/account.xml b/addons/website_account/security/account.xml new file mode 100644 index 00000000000..1a3889d8280 --- /dev/null +++ b/addons/website_account/security/account.xml @@ -0,0 +1,18 @@ + + + + + Portal Personal Account Invoices + + [('message_partner_ids','child_of',[user.commercial_partner_id.id])] + + + + + Portal Invoice Lines + + [('invoice_id.message_partner_ids','child_of',[user.commercial_partner_id.id])] + + + + diff --git a/addons/website_account/security/ir.model.access.csv b/addons/website_account/security/ir.model.access.csv new file mode 100644 index 00000000000..6bf20086200 --- /dev/null +++ b/addons/website_account/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_invoice,account.invoice,account.model_account_invoice,base.group_portal,1,0,0,0 +access_account_invoice_line,account.invoice.line,account.model_account_invoice_line,base.group_portal,1,0,0,0 \ No newline at end of file diff --git a/addons/website_account/views/account_templates.xml b/addons/website_account/views/account_templates.xml new file mode 100644 index 00000000000..ad9cd8c46b6 --- /dev/null +++ b/addons/website_account/views/account_templates.xml @@ -0,0 +1,74 @@ + + + + + + + \ No newline at end of file diff --git a/addons/website_account/views/account_views.xml b/addons/website_account/views/account_views.xml new file mode 100644 index 00000000000..55f659686fc --- /dev/null +++ b/addons/website_account/views/account_views.xml @@ -0,0 +1,13 @@ + + + + account.invoice.select.share + account.invoice + + + + [('share','=', False)] + + + + diff --git a/addons/website_event_sale/static/src/js/website.tour.event_sale.js b/addons/website_event_sale/static/src/js/website.tour.event_sale.js index f6638573256..8a29796dab4 100644 --- a/addons/website_event_sale/static/src/js/website.tour.event_sale.js +++ b/addons/website_event_sale/static/src/js/website.tour.event_sale.js @@ -86,7 +86,7 @@ tour.register('event_buy_tickets', { { content: "Pay", extra_trigger: '#payment_method label:contains(Wire Transfer) input:checked', - trigger: '.oe_sale_acquirer_button .btn[type="submit"]:visible', + trigger: '.o_payment_acquirer_button .btn[type="submit"]:visible', }, { content: "Last step", diff --git a/addons/website_payment/models/__init__.py b/addons/website_payment/models/__init__.py index 300cd3e8706..bd190fa80b4 100644 --- a/addons/website_payment/models/__init__.py +++ b/addons/website_payment/models/__init__.py @@ -1,2 +1 @@ -from . import payment from . import website diff --git a/addons/website_payment/models/payment.py b/addons/website_payment/models/payment.py deleted file mode 100644 index cc7deca5556..00000000000 --- a/addons/website_payment/models/payment.py +++ /dev/null @@ -1,7 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models - -class PaymentAcquirer(models.Model): - _name = 'payment.acquirer' - _inherit = ['payment.acquirer','website.published.mixin'] diff --git a/addons/website_portal_sale/__manifest__.py b/addons/website_portal_sale/__manifest__.py index cb2311347e5..0c46f60dd96 100644 --- a/addons/website_portal_sale/__manifest__.py +++ b/addons/website_portal_sale/__manifest__.py @@ -4,20 +4,21 @@ { 'name': 'Website Portal for Sales', 'category': 'Website', - 'summary': 'Add your sales document in the frontend portal (sales order, quotations, invoices)', + 'summary': 'Add your sales document in the frontend portal (sales order, quotations)', 'version': '1.0', 'description': """ -Add your sales document in the frontend portal. Your customers will be able to connect to their portal to see the list (and the state) of their invoices (pdf report), sales orders and quotations (web pages). +Add your sales document in the frontend portal. Your customers will be able to connect to their portal to see the list (and the state) of their sales orders and quotations (web pages). """, 'depends': [ 'sale', + 'sale_payment', 'website_portal', 'website_payment', ], 'data': [ 'views/crm_team_views.xml', - 'views/website_portal_sale_templates.xml', 'views/sale_order_views.xml', + 'views/website_portal_sale_templates.xml', 'security/ir.model.access.csv', 'security/sale_order_security.xml', ], diff --git a/addons/website_portal_sale/controllers/main.py b/addons/website_portal_sale/controllers/main.py index 0edaffb85de..0b7a1e18724 100644 --- a/addons/website_portal_sale/controllers/main.py +++ b/addons/website_portal_sale/controllers/main.py @@ -15,7 +15,6 @@ class website_account(website_account): partner = request.env.user.partner_id SaleOrder = request.env['sale.order'] - Invoice = request.env['account.invoice'] quotation_count = SaleOrder.search_count([ ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), ('state', 'in', ['sent', 'cancel']) @@ -24,16 +23,10 @@ class website_account(website_account): ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), ('state', 'in', ['sale', 'done']) ]) - invoice_count = Invoice.search_count([ - ('type', 'in', ['out_invoice', 'out_refund']), - ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), - ('state', 'in', ['open', 'paid', 'cancel']) - ]) values.update({ 'quotation_count': quotation_count, 'order_count': order_count, - 'invoice_count': invoice_count, }) return values @@ -160,89 +153,3 @@ class website_account(website_account): } values.update(get_records_pager(history, order)) return request.render("website_portal_sale.orders_followup", values) - - # - # Invoices - # - - @http.route(['/my/invoices', '/my/invoices/page/'], type='http', auth="user", website=True) - def portal_my_invoices(self, page=1, date_begin=None, date_end=None, sortby=None, **kw): - values = self._prepare_portal_layout_values() - partner = request.env.user.partner_id - AccountInvoice = request.env['account.invoice'] - - domain = [ - ('type', 'in', ['out_invoice', 'out_refund']), - ('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]), - ('state', 'in', ['open', 'paid', 'cancelled']) - ] - - searchbar_sortings = { - 'date': {'label': _('Invoice Date'), 'order': 'date_invoice desc'}, - 'duedate': {'label': _('Due Date'), 'order': 'date_due desc'}, - 'name': {'label': _('Reference'), 'order': 'name desc'}, - 'state': {'label': _('Status'), 'order': 'state'}, - } - # default sort by order - if not sortby: - sortby = 'date' - order = searchbar_sortings[sortby]['order'] - - archive_groups = self._get_archive_groups('account.invoice', domain) - if date_begin and date_end: - domain += [('create_date', '>', date_begin), ('create_date', '<=', date_end)] - - # count for pager - invoice_count = AccountInvoice.search_count(domain) - # pager - pager = request.website.pager( - url="/my/invoices", - url_args={'date_begin': date_begin, 'date_end': date_end, 'sortby': sortby}, - total=invoice_count, - page=page, - step=self._items_per_page - ) - # content according to pager and archive selected - invoices = AccountInvoice.search(domain, order=order, limit=self._items_per_page, offset=pager['offset']) - values.update({ - 'date': date_begin, - 'invoices': invoices, - 'page_name': 'invoice', - 'pager': pager, - 'archive_groups': archive_groups, - 'default_url': '/my/invoices', - 'searchbar_sortings': searchbar_sortings, - 'sortby': sortby, - }) - return request.render("website_portal_sale.portal_my_invoices", values) - - @http.route(['/my/invoices/'], type='http', auth="user", website=True) - def portal_my_invoices_report(self, invoice_id, **kw): - invoice = request.env['account.invoice'].browse(invoice_id) - try: - invoice.check_access_rights('read') - invoice.check_access_rule('read') - except AccessError: - return request.render("website.403") - # print report as sudo, since it require access to taxes, payment term, ... and portal - # does not have those access rights. - pdf = request.env.ref('account.account_invoices').sudo().render_qweb_pdf([invoice_id])[0] - pdfhttpheaders = [ - ('Content-Type', 'application/pdf'), - ('Content-Length', len(pdf)), - ] - return request.make_response(pdf, headers=pdfhttpheaders) - - def details_form_validate(self, data): - error, error_message = super(website_account, self).details_form_validate(data) - # prevent VAT/name change if invoices exist - partner = request.env['res.users'].browse(request.uid).partner_id - invoices = request.env['account.invoice'].sudo().search_count([('partner_id', '=', partner.id), ('state', 'not in', ['draft', 'cancel'])]) - if invoices: - if 'vat' in partner and (data['vat'] or False) != (partner.vat or False): - error['vat'] = 'error' - error_message.append(_('Changing VAT number is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) - if 'name' in data and (data['name'] or False) != (partner.name or False): - error['name'] = 'error' - error_message.append(_('Changing your name is not allowed once invoices have been issued for your account. Please contact us directly for this operation.')) - return error, error_message diff --git a/addons/website_portal_sale/models/__init__.py b/addons/website_portal_sale/models/__init__.py index 9aa8c7def03..34290388641 100644 --- a/addons/website_portal_sale/models/__init__.py +++ b/addons/website_portal_sale/models/__init__.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from . import account_invoice from . import crm_team from . import sale_order -from . import payment diff --git a/addons/website_portal_sale/models/payment.py b/addons/website_portal_sale/models/payment.py deleted file mode 100644 index 7d442871c5d..00000000000 --- a/addons/website_portal_sale/models/payment.py +++ /dev/null @@ -1,89 +0,0 @@ -# -*- coding: utf-8 -*- -# Part of Odoo. See LICENSE file for full copyright and licensing details. -import logging -from odoo import api, fields, models -from odoo.tools import float_compare - -_logger = logging.getLogger(__name__) - - -class PaymentTransaction(models.Model): - _inherit = 'payment.transaction' - - # link with the sales order - # YTI FIXME: The auto_join seems useless - sale_order_id = fields.Many2one('sale.order', string='Sales Order', auto_join=True) - - def _generate_and_pay_invoice(self, tx, acquirer_name): - tx.sale_order_id._force_lines_to_invoice_policy_order() - - # force company to ensure journals/accounts etc. are correct - # company_id needed for default_get on account.journal - # force_company needed for company_dependent fields - ctx_company = {'company_id': tx.sale_order_id.company_id.id, - 'force_company': tx.sale_order_id.company_id.id} - created_invoice = tx.sale_order_id.with_context(**ctx_company).action_invoice_create() - created_invoice = self.env['account.invoice'].browse(created_invoice).with_context(**ctx_company) - - if created_invoice: - _logger.info('<%s> transaction completed, auto-generated invoice %s (ID %s) for %s (ID %s)', - acquirer_name, created_invoice.name, created_invoice.id, tx.sale_order_id.name, tx.sale_order_id.id) - - created_invoice.action_invoice_open() - if not tx.acquirer_id.journal_id: - default_journal = self.env['account.journal'].search([('type', '=', 'bank')], limit=1) - if not default_journal: - _logger.warning('<%s> transaction completed, could not auto-generate payment for %s (ID %s) (no journal set on acquirer)', - acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) - tx.acquirer_id.journal_id = default_journal - created_invoice.pay_and_reconcile(tx.acquirer_id.journal_id, pay_amount=created_invoice.amount_total) - if created_invoice.payment_ids: - created_invoice.payment_ids[0].payment_transaction_id = tx - else: - _logger.warning('<%s> transaction completed, could not auto-generate invoice for %s (ID %s)', - acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) - - @api.model - def form_feedback(self, data, acquirer_name): - """ Override to confirm the sales order, if defined, and if the transaction - is done. """ - tx = None - res = super(PaymentTransaction, self).form_feedback(data, acquirer_name) - - # fetch the tx - tx_find_method_name = '_%s_form_get_tx_from_data' % acquirer_name - if hasattr(self, tx_find_method_name): - tx = getattr(self, tx_find_method_name)(data) - _logger.info('<%s> transaction processed: tx ref:%s, tx amount: %s', acquirer_name, tx.reference if tx else 'n/a', tx.amount if tx else 'n/a') - - if tx: - # Auto-confirm SO if necessary - tx._confirm_so(acquirer_name=acquirer_name) - - return res - - def _confirm_so(self, acquirer_name=False): - for tx in self: - try: - # check tx state, confirm the potential SO - if tx.sale_order_id and tx.sale_order_id.state in ['draft', 'sent']: - # verify SO/TX match, excluding tx.fees which are currently not included in SO - amount_matches = float_compare(tx.amount, tx.sale_order_id.amount_total, 2) == 0 - if amount_matches: - if not acquirer_name: - acquirer_name = tx.sale_order_id.payment_acquirer_id.provider or 'unknown' - if tx.state == 'authorized' and tx.acquirer_id.capture_manually: - _logger.info('<%s> transaction authorized, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) - tx.sale_order_id.with_context(send_email=True).action_confirm() - if tx.state == 'done': - _logger.info('<%s> transaction completed, auto-confirming order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) - tx.sale_order_id.with_context(send_email=True).action_confirm() - self._generate_and_pay_invoice(tx, acquirer_name) - elif tx.state not in ['cancel', 'error'] and tx.sale_order_id.state == 'draft': - _logger.info('<%s> transaction pending/to confirm manually, sending quote email for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) - tx.sale_order_id.force_quotation_send() - else: - _logger.warning('<%s> transaction MISMATCH for order %s (ID %s)', acquirer_name, tx.sale_order_id.name, tx.sale_order_id.id) - - except Exception: - _logger.exception('Fail to confirm the order or send the confirmation email%s', tx and ' for the transaction %s' % tx.reference or '') diff --git a/addons/website_portal_sale/security/ir.model.access.csv b/addons/website_portal_sale/security/ir.model.access.csv index 2033898fc32..fa1ee2d0443 100644 --- a/addons/website_portal_sale/security/ir.model.access.csv +++ b/addons/website_portal_sale/security/ir.model.access.csv @@ -1,5 +1,3 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_sale_order,sale.order,sale.model_sale_order,base.group_portal,1,0,0,0 -access_sale_order_line,sale.order.line,sale.model_sale_order_line,base.group_portal,1,0,0,0 -access_account_invoice,account.invoice,account.model_account_invoice,base.group_portal,1,0,0,0 -access_account_invoice_line,account.invoice.line,account.model_account_invoice_line,base.group_portal,1,0,0,0 +access_sale_order_line,sale.order.line,sale.model_sale_order_line,base.group_portal,1,0,0,0 \ No newline at end of file diff --git a/addons/website_portal_sale/security/sale_order_security.xml b/addons/website_portal_sale/security/sale_order_security.xml index aa97cba3ea0..8f5b5b4cc80 100644 --- a/addons/website_portal_sale/security/sale_order_security.xml +++ b/addons/website_portal_sale/security/sale_order_security.xml @@ -1,7 +1,6 @@ - Portal Personal Quotations/Sales Orders @@ -20,21 +19,5 @@ [('order_id.message_partner_ids','child_of',[user.commercial_partner_id.id])] - - - - Portal Personal Account Invoices - - [('message_partner_ids','child_of',[user.commercial_partner_id.id])] - - - - - Portal Invoice Lines - - [('invoice_id.message_partner_ids','child_of',[user.commercial_partner_id.id])] - - - diff --git a/addons/website_portal_sale/static/src/less/website_portal_sale.less b/addons/website_portal_sale/static/src/less/website_portal_sale.less index dfdc8ec75f2..dd3528e3628 100644 --- a/addons/website_portal_sale/static/src/less/website_portal_sale.less +++ b/addons/website_portal_sale/static/src/less/website_portal_sale.less @@ -8,7 +8,3 @@ .orders_label_text_align { vertical-align: 15%; } - -.cell_overflow { - overflow: hidden; -} diff --git a/addons/website_portal_sale/views/crm_team_views.xml b/addons/website_portal_sale/views/crm_team_views.xml index 9355e29a7fe..02629ca02dd 100644 --- a/addons/website_portal_sale/views/crm_team_views.xml +++ b/addons/website_portal_sale/views/crm_team_views.xml @@ -1,22 +1,6 @@ - - Pending Payment Transactions - payment.transaction - form - tree,form - [('sale_order_id.team_id', '=', active_id), ('state', '=', 'pending')] - - - - Authorized Payment Transactions - payment.transaction - form - tree,form - [('sale_order_id.team_id', '=', active_id), ('state', '=', 'authorized')] - - crm.team.kanban crm.team @@ -27,7 +11,7 @@
@@ -38,7 +22,7 @@
diff --git a/addons/website_portal_sale/views/sale_order_views.xml b/addons/website_portal_sale/views/sale_order_views.xml index 0a06daf69fd..002866c781f 100644 --- a/addons/website_portal_sale/views/sale_order_views.xml +++ b/addons/website_portal_sale/views/sale_order_views.xml @@ -1,16 +1,5 @@ - - account.invoice.select.share - account.invoice - - - - [('share','=', False)] - - - - sale.order.form sale.order diff --git a/addons/website_portal_sale/views/website_portal_sale_templates.xml b/addons/website_portal_sale/views/website_portal_sale_templates.xml index 5e3eec666da..8a67a279532 100644 --- a/addons/website_portal_sale/views/website_portal_sale_templates.xml +++ b/addons/website_portal_sale/views/website_portal_sale_templates.xml @@ -17,14 +17,6 @@ - - - - - -