1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
# |
3
|
|
|
# This file is part of SENAITE.CORE |
4
|
|
|
# |
5
|
|
|
# Copyright 2018 by it's authors. |
6
|
|
|
# Some rights reserved. See LICENSE.rst, CONTRIBUTORS.rst. |
7
|
|
|
|
8
|
|
|
from bika.lims import _ |
9
|
|
|
from bika.lims import api |
10
|
|
|
from bika.lims.browser import BrowserView |
11
|
|
|
from bika.lims.interfaces import IAnalysis |
12
|
|
|
from bika.lims.interfaces import IAnalysisProfile |
13
|
|
|
from bika.lims.interfaces import IInvoiceView |
14
|
|
|
from bika.lims.utils import createPdf |
15
|
|
|
from plone.memoize import view |
16
|
|
|
from Products.CMFPlone.i18nl10n import ulocalized_time |
17
|
|
|
from Products.Five.browser.pagetemplatefile import ViewPageTemplateFile |
18
|
|
|
from senaite.core.supermodel.model import SuperModel |
19
|
|
|
from zope.i18n.locales import locales |
20
|
|
|
from zope.interface import implements |
21
|
|
|
|
22
|
|
|
|
23
|
|
|
class InvoiceView(BrowserView): |
24
|
|
|
"""Analyses Invoice View |
25
|
|
|
""" |
26
|
|
|
implements(IInvoiceView) |
27
|
|
|
|
28
|
|
|
template = ViewPageTemplateFile("templates/invoice.pt") |
29
|
|
|
print_template = ViewPageTemplateFile("templates/invoice_print.pt") |
30
|
|
|
content = ViewPageTemplateFile("templates/invoice_content.pt") |
31
|
|
|
|
32
|
|
|
def __init__(self, context, request): |
33
|
|
|
self.context = context |
34
|
|
|
self.request = request |
35
|
|
|
|
36
|
|
|
def __call__(self): |
37
|
|
|
# TODO: Refactor to permission |
38
|
|
|
# Control the visibility of the invoice create/print document actions |
39
|
|
|
if api.get_review_status(self.context) in ["verified", "published"]: |
40
|
|
|
self.request["verified"] = 1 |
41
|
|
|
return self.template() |
42
|
|
|
|
43
|
|
|
@property |
44
|
|
|
def sample(self): |
45
|
|
|
"""Returns a supermodel of the sample |
46
|
|
|
""" |
47
|
|
|
return SuperModel(self.context) |
48
|
|
|
|
49
|
|
|
def to_localized_time(self, date, **kw): |
50
|
|
|
"""Converts the given date to a localized time string |
51
|
|
|
""" |
52
|
|
|
if date is None: |
53
|
|
|
return "" |
54
|
|
|
# default options |
55
|
|
|
options = { |
56
|
|
|
"long_format": True, |
57
|
|
|
"time_only": False, |
58
|
|
|
"context": api.get_portal(), |
59
|
|
|
"request": api.get_request(), |
60
|
|
|
"domain": "senaite.core", |
61
|
|
|
} |
62
|
|
|
options.update(kw) |
63
|
|
|
return ulocalized_time(date, **options) |
64
|
|
|
|
65
|
|
|
@view.memoize |
66
|
|
|
def get_currency_symbol(self): |
67
|
|
|
"""Get the currency Symbol |
68
|
|
|
""" |
69
|
|
|
locale = locales.getLocale("en") |
70
|
|
|
setup = api.get_setup() |
71
|
|
|
currency = setup.getCurrency() |
72
|
|
|
return locale.numbers.currencies[currency].symbol |
73
|
|
|
|
74
|
|
|
@view.memoize |
75
|
|
|
def get_decimal_mark(self): |
76
|
|
|
"""Returns the decimal mark |
77
|
|
|
""" |
78
|
|
|
setup = api.get_setup() |
79
|
|
|
return setup.getDecimalMark() |
80
|
|
|
|
81
|
|
|
def format_price(self, price): |
82
|
|
|
"""Formats the price with the set decimal mark and currency |
83
|
|
|
""" |
84
|
|
|
# ensure we have a float |
85
|
|
|
price = api.to_float(price, default=0.0) |
86
|
|
|
dm = self.get_decimal_mark() |
87
|
|
|
cur = self.get_currency_symbol() |
88
|
|
|
price = "%s %.2f" % (cur, price) |
89
|
|
|
return price.replace(".", dm) |
90
|
|
|
|
91
|
|
|
def get_billable_items(self): |
92
|
|
|
"""Return a list of billable items |
93
|
|
|
""" |
94
|
|
|
items = [] |
95
|
|
|
for obj in self.context.getBillableItems(): |
96
|
|
|
if self.is_profile(obj): |
97
|
|
|
items.append({ |
98
|
|
|
"obj": obj, |
99
|
|
|
"title": obj.Title(), |
100
|
|
|
"vat": obj.getAnalysisProfileVAT(), |
101
|
|
|
"price": self.format_price(obj.getAnalysisProfilePrice()), |
102
|
|
|
}) |
103
|
|
|
if self.is_analysis(obj): |
104
|
|
|
items.append({ |
105
|
|
|
"obj": obj, |
106
|
|
|
"title": obj.Title(), |
107
|
|
|
"vat": obj.getVAT(), |
108
|
|
|
"price": self.format_price(obj.getPrice()), |
109
|
|
|
}) |
110
|
|
|
return items |
111
|
|
|
|
112
|
|
|
def is_profile(self, obj): |
113
|
|
|
"""Checks if the object is a profile |
114
|
|
|
""" |
115
|
|
|
return IAnalysisProfile.providedBy(obj) |
116
|
|
|
|
117
|
|
|
def is_analysis(self, obj): |
118
|
|
|
"""Checks if the object is an analysis |
119
|
|
|
""" |
120
|
|
|
return IAnalysis.providedBy(obj) |
121
|
|
|
|
122
|
|
|
def add_status_message(self, message, level="info"): |
123
|
|
|
"""Set a portal status message |
124
|
|
|
""" |
125
|
|
|
return self.context.plone_utils.addPortalMessage(message, level) |
126
|
|
|
|
127
|
|
|
|
128
|
|
View Code Duplication |
class InvoicePrintView(InvoiceView): |
|
|
|
|
129
|
|
|
"""Print view w/o outer contents |
130
|
|
|
""" |
131
|
|
|
template = ViewPageTemplateFile("templates/invoice_print.pt") |
132
|
|
|
|
133
|
|
|
def __call__(self): |
134
|
|
|
pdf = self.create_pdf() |
135
|
|
|
filename = "{}.pdf".format(self.context.getId()) |
136
|
|
|
return self.download(pdf, filename) |
137
|
|
|
|
138
|
|
|
def create_pdf(self): |
139
|
|
|
"""Create the invoice PDF |
140
|
|
|
""" |
141
|
|
|
invoice = self.template() |
142
|
|
|
return createPdf(invoice) |
143
|
|
|
|
144
|
|
|
def download(self, data, filename, content_type="application/pdf"): |
145
|
|
|
"""Download the PDF |
146
|
|
|
""" |
147
|
|
|
self.request.response.setHeader( |
148
|
|
|
"Content-Disposition", "inline; filename=%s" % filename) |
149
|
|
|
self.request.response.setHeader("Content-Type", content_type) |
150
|
|
|
self.request.response.setHeader("Content-Length", len(data)) |
151
|
|
|
self.request.response.setHeader("Cache-Control", "no-store") |
152
|
|
|
self.request.response.setHeader("Pragma", "no-cache") |
153
|
|
|
self.request.response.write(data) |
154
|
|
|
|
155
|
|
|
|
156
|
|
|
class InvoiceCreate(InvoicePrintView): |
157
|
|
|
"""Create the invoice |
158
|
|
|
""" |
159
|
|
|
|
160
|
|
|
def __call__(self): |
161
|
|
|
# Create the invoice object and link it to the current AR. |
162
|
|
|
pdf = self.create_pdf() |
163
|
|
|
invoice = self.context.createInvoice(pdf) |
164
|
|
|
self.add_status_message(_("Invoice {} created").format( |
165
|
|
|
api.get_id(invoice))) |
166
|
|
|
|
167
|
|
|
# Reload the page to see the the new fields |
168
|
|
|
self.request.response.redirect( |
169
|
|
|
"%s/invoice" % self.aq_parent.absolute_url()) |
170
|
|
|
|