Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[10.0][IMP] shopinvader: Allows to download paid invoices for confirmed sal… #366

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion shopinvader/controllers/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from odoo.addons.base_rest.controllers import main
from odoo.exceptions import MissingError
from odoo.http import request
from odoo.http import request, route

_logger = logging.getLogger(__name__)

Expand All @@ -18,6 +18,13 @@ class InvaderController(main.RestController):
_collection_name = "shopinvader.backend"
_default_auth = "api_key"

@route(["/shopinvader/invoice/<int:_id>/download"], methods=["GET"])
def invoice_download(self, _id=None, **params):
params["id"] = _id
return self._process_method(
"invoice", "download", _id=_id, params=params
)

@classmethod
def _get_partner_from_headers(cls, headers):
partner_model = request.env["shopinvader.partner"]
Expand Down
1 change: 1 addition & 0 deletions shopinvader/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
from . import sale
from . import address
from . import customer
from . import invoice
14 changes: 0 additions & 14 deletions shopinvader/services/abstract_mail.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,6 @@ def ask_email(self, _id):
def _validator_ask_email(self):
return {}

def _is_logged(self):
"""
Check if the current partner is a real partner (not the anonymous one
and not empty)
:return: bool
"""
logged = False
if (
self.partner
and self.partner != self.shopinvader_backend.anonymous_partner_id
):
logged = True
return logged

def _get_email_notification_type(self, record):
"""
Based on the given record, get the notification type.
Expand Down
136 changes: 136 additions & 0 deletions shopinvader/services/invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# -*- coding: utf-8 -*-
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import mimetypes

from odoo import _
from odoo.addons.base_rest.components.service import (
skip_secure_response,
to_int,
)
from odoo.addons.component.core import Component
from odoo.exceptions import MissingError
from odoo.http import content_disposition, request
from odoo.osv import expression


class InvoiceService(Component):
_inherit = "base.shopinvader.service"
_name = "shopinvader.invoice.service"
_usage = "invoice"
_expose_model = "account.invoice"
_description = "Service providing a method to download invoices"

# The following method are 'public' and can be called from the controller.
# All params are untrusted so please check it !

@skip_secure_response
def download(self, _id, **params):
"""
Get invoice file. This method is also callable by HTTP GET
"""
invoice = self._get(_id)
headers, content = self._get_binary_content(invoice)
if not content:
raise MissingError(_("No image found for partner %s") % _id)
response = request.make_response(content, headers)
response.status_code = 200
return response

def to_openapi(self):
res = super(InvoiceService, self).to_openapi()
# Manually add route for HTTP GET download
response = self._get_openapi_default_responses()
response["200"] = {"description": "The file to download"}
parameters = self._get_openapi_default_parameters()
parameters.append(
{
"schema": {"type": "integer"},
"description": "Item id",
"required": True,
"name": "id",
"in": "path",
}
)
res["paths"]["/{id}/download"] = {
"get": {
"responses": response,
"parameters": parameters,
"summary": "Get the invoice file",
}
}
return res

# Validator

def _validator_download(self):
return {"id": {"type": "integer", "required": True, "coerce": to_int}}

# Private implementation

def _get_base_search_domain(self):
"""
This method must provide a domain used to retrieve the requested
invoice.

This domain MUST TAKE CARE of restricting the access to the invoices
visible for the current customer
:return: Odoo domain
"""
# The partner must be set and not be the anonymous one
if not self._is_logged():
return expression.FALSE_DOMAIN

# here we only allow access to invoices linked to a sale order of the
# current customer
so_domain = [
("partner_id", "=", self.partner.id),
("shopinvader_backend_id", "=", self.shopinvader_backend.id),
("typology", "=", "sale"),
]
# invoice_ids on sale.order is a computed field...
# to avoid to duplicate the logic, we search for the sale oders
# and check if the invoice_id is into the list of sale.invoice_ids
sales = self.env["sale.order"].search(so_domain)
invoice_ids = sales.mapped("invoice_ids").ids
return expression.normalize_domain(
[("id", "in", invoice_ids), ("state", "=", "paid")]
)

def _get_binary_content(self, invoice):
"""
Generate the invoice report....
:param invoice:
:returns: (headers, content)
"""
# ensure the report is generated
invoice_report_def = invoice.invoice_print()
report_name = invoice_report_def["report_name"]
report_type = invoice_report_def["report_type"]
content, format = self.env["ir.actions.report.xml"].render_report(
res_ids=invoice.ids,
name=report_name,
data={"report_type": report_type},
)
report = self._get_report(report_name, report_type)
filename = self._get_binary_content_filename(invoice, report, format)
mimetype = mimetypes.guess_type(filename)
if mimetype:
mimetype = mimetype[0]
headers = [
("Content-Type", mimetype),
("X-Content-Type-Options", "nosniff"),
("Content-Disposition", content_disposition(filename)),
("Content-Length", len(content)),
]
return headers, content

def _get_report(self, report_name, report_type):
domain = [
("report_type", "=", report_type),
("report_name", "=", report_name),
]
return self.env["ir.actions.report.xml"].search(domain)

def _get_binary_content_filename(self, invoice, report, format):
return "{}.{}".format(report.name, format)
18 changes: 18 additions & 0 deletions shopinvader/services/sale.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,21 @@ def _launch_notification(self, target, notif_type):
return super(SaleService, self)._launch_notification(
target, notif_type
)

def _convert_one_sale(self, sale):
res = super(SaleService, self)._convert_one_sale(sale)
res["invoices"] = self._convert_invoices(sale.sudo())
return res

def _convert_invoices(self, sale):
res = []
for invoice in sale.invoice_ids.filtered(lambda i: i.state == "paid"):
res.append(self._convert_one_invoice(invoice))
return res

def _convert_one_invoice(self, invoice):
return {
"id": invoice.id,
"name": invoice.number,
"date": invoice.date_invoice or None,
}
14 changes: 14 additions & 0 deletions shopinvader/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,20 @@ def _get_openapi_default_parameters(self):
)
return defaults

def _is_logged(self):
"""
Check if the current partner is a real partner (not the anonymous one
and not empty)
:return: bool
"""
logged = False
if (
self.partner
and self.partner != self.shopinvader_backend.anonymous_partner_id
):
logged = True
return logged

@property
def shopinvader_response(self):
"""
Expand Down
3 changes: 2 additions & 1 deletion shopinvader/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from . import test_shopinvader_category_binding_wizard
from . import test_customer
from . import test_shopinvader_partner
from . import test_res_partner
from . import test_shopinvader_variant_seo_title
from . import test_shopinvader_partner_binding
from . import test_res_partner
from . import test_invoice
110 changes: 110 additions & 0 deletions shopinvader/tests/test_invoice.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# -*- coding: utf-8 -*-
# Copyright 2019 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import mock
from odoo import fields
from odoo.exceptions import MissingError

from .common import CommonCase


class TestInvoice(CommonCase):
def setUp(self, *args, **kwargs):
super(TestInvoice, self).setUp(*args, **kwargs)
self.register_payments_obj = self.env["account.register.payments"]
self.journal_obj = self.env["account.journal"]
self.sale = self.env.ref("shopinvader.sale_order_2")
self.partner = self.env.ref("shopinvader.partner_1")
self.payment_method_manual_in = self.env.ref(
"account.account_payment_method_manual_in"
)
self.bank_journal_euro = self.journal_obj.create(
{"name": "Bank", "type": "bank", "code": "BNK627"}
)
with self.work_on_services(partner=self.partner) as work:
self.sale_service = work.component(usage="sales")
self.invoice_service = work.component(usage="invoice")
self.invoice = self._confirm_and_invoice_sale(self.sale)

def _make_payment(self, invoice):
"""
Make the invoice payment
:param invoice: account.invoice recordset
:return: bool
"""
ctx = {"active_model": invoice._name, "active_ids": invoice.ids}
wizard_obj = self.register_payments_obj.with_context(ctx)
register_payments = wizard_obj.create(
{
"payment_date": fields.Date.today(),
"journal_id": self.bank_journal_euro.id,
"payment_method_id": self.payment_method_manual_in.id,
}
)
register_payments.create_payment()

def _confirm_and_invoice_sale(self, sale):
sale.action_confirm()
for line in sale.order_line:
line.write({"qty_delivered": line.product_uom_qty})
invoice_id = sale.action_invoice_create()
invoice = self.env["account.invoice"].browse(invoice_id)
invoice.action_invoice_open()
invoice.action_move_create()
return invoice

def test_01(self):
"""
Data
* A confirmed sale order with an invoice not yet paid
Case:
* Try to download the image
Expected result:
* MissingError should be raised
"""
with self.assertRaises(MissingError):
self.invoice_service.download(self.invoice.id)

def test_02(self):
"""
Data
* A confirmed sale order with a paid invoice
Case:
* Try to download the image
Expected result:
* An http response with the file to download
"""
self._make_payment(self.invoice)
with mock.patch(
"openerp.addons.shopinvader.services.invoice.content_disposition"
) as mocked_cd, mock.patch(
"openerp.addons.shopinvader.services.invoice.request"
) as mocked_request:
mocked_cd.return_value = "attachment; filename=test"
make_response = mock.MagicMock()
mocked_request.make_response = make_response
self.invoice_service.download(self.invoice.id)
self.assertEqual(1, make_response.call_count)
content, headers = make_response.call_args[0]
self.assertTrue(content)
self.assertIn(
("Content-Disposition", "attachment; filename=test"), headers
)

def test_03(self):
"""
Data
* A confirmed sale order with a paid invoice but not for the
current customer
Case:
* Try to download the image
Expected result:
* MissingError should be raised
"""
sale = self.env.ref("sale.sale_order_1")
sale.shopinvader_backend_id = self.backend
self.assertNotEqual(sale.partner_id, self.partner)
invoice = self._confirm_and_invoice_sale(sale)
self._make_payment(invoice)
with self.assertRaises(MissingError):
self.invoice_service.download(invoice.id)
Loading