media ok + test

This commit is contained in:
Frederik Jaeckel 2022-10-14 15:29:33 +02:00
parent 0100653643
commit 2933a217e2
13 changed files with 565 additions and 33 deletions

3
.hgignore Normal file
View file

@ -0,0 +1,3 @@
syntax: glob
__pycache__/*
locale/convert_de2en.py

View file

@ -4,39 +4,9 @@
# full copyright notices and license terms.
from trytond.pool import Pool
from .book import Book
from .types import Type
from .line import Line, LineContext
from .splitline import SplitLine
from .wizard_openline import OpenCashBook, OpenCashBookStart, OpenCashBookTree
from .wizard_runreport import RunCbReport, RunCbReportStart
from .wizard_booking import EnterBookingWizard, EnterBookingStart
from .configuration import Configuration, UserConfiguration
from .category import Category
from .reconciliation import Reconciliation
from .cbreport import ReconciliationReport
from .line import Line
def register():
Pool.register(
Configuration,
UserConfiguration,
Type,
Category,
Book,
LineContext,
Line,
SplitLine,
Reconciliation,
OpenCashBookStart,
RunCbReportStart,
EnterBookingStart,
module='cashbook', type_='model')
Pool.register(
ReconciliationReport,
module='cashbook', type_='report')
Pool.register(
OpenCashBook,
OpenCashBookTree,
RunCbReport,
EnterBookingWizard,
module='cashbook', type_='wizard')
module='cashbook_media', type_='model')

165
line.py Normal file
View file

@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
import mimetypes, magic
from io import BytesIO
from PIL import Image
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.config import config
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
from trytond.pyson import Eval, Bool
from trytond.modules.cashbook.line import STATES, DEPENDS
store_prefix = config.get('cashbook', 'store_prefix', default='cashbook')
image_limit = config.get('cashbook', 'image_max_pixel', default='2000')
try :
image_limit = int(image_limit)
if image_limit < 100:
image_limit = 100
if image_limit > 10000:
image_limit = 10000
except :
image_limit = 2000
class Line(metaclass=PoolMeta):
__name__ = 'cashbook.line'
media = fields.Binary(string='Image of PDF', filename='media_name',
file_id='media_id', store_prefix=store_prefix,
states=STATES, depends=DEPENDS)
media_name = fields.Char(string='File name',
states={
'required': Bool(Eval('media')),
'readonly': STATES['readonly'],
}, depends=DEPENDS)
media_id = fields.Char(string='File ID', readonly=True)
media_mime = fields.Char(string='MIME', readonly=True)
media_size = fields.Integer(string='File size', readonly=True)
@classmethod
def _identify_file(cls, data, mime=True):
""" get file-type
"""
return magic.from_buffer(data, mime=mime)
@classmethod
def _hr_file_size(cls, num, suffix="B"):
"""
"""
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
return f"{num:3.1f}{unit}{suffix}"
num /= 1024.0
return f"{num:.1f}Yi{suffix}"
@classmethod
def resize_image_file(cls, image_data):
""" shrink image 'image_limit' pixel if its bigger
"""
image_data2 = None
with BytesIO(image_data) as fhdl:
image = Image.open(fhdl, 'r')
(width, height) = image.size
if (width > image_limit) or (height > image_limit):
if width > height:
new_size = (image_limit, int(height * image_limit / width))
else :
new_size = (int(width * image_limit / height), image_limit)
# resize - fit in (image_limit x image_limit)
img2 = image.resize(new_size, Image.LANCZOS)
with BytesIO() as fhdl2:
img2.save(fhdl2, 'JPEG', optimize=True, quality=80)
fhdl2.seek(0)
image_data2 = fhdl2.read()
del img2
del image
return image_data2
@classmethod
def get_media_info(cls, values):
""" get mime-type, update file-name
"""
if len(values['media'] or '') < 100:
values['media'] = None
values['media_mime'] = None
values['media_size'] = None
values['media_name'] = None
else :
values['media_mime'] = cls._identify_file(values['media'][:1024])
# if its a image, resize it to fit in (image_limit x image_limit) pixel
if values['media_mime'].startswith('image'):
new_image = cls.resize_image_file(values['media'])
if new_image is not None:
values['media'] = new_image
values['media_mime'] = cls._identify_file(values['media'][:1024])
values['media_size'] = len(values['media'])
file_ext = mimetypes.guess_extension(values['media_mime'])
if 'media_name' in values.keys():
if not values['media_name'].endswith(file_ext):
# cut extension
if values['media_name'][-4] == '.':
values['media_name'] = values['media_name'][:-4]
values['media_name'] = values['media_name'] + file_ext
return values
@classmethod
def validate(cls, lines):
""" deny invalid mime-types, file-sizes etc.
"""
super(Line, cls).validate(lines)
for line in lines:
if line.media is not None:
if line.media_size > 1024*1024*5:
raise UserError(gettext(
'cashbook_media.msg_file_too_big',
recname = line.rec_name,
))
if not line.media_mime in ['application/pdf',
'image/png', 'image/jpg', 'image/jpeg']:
raise UserError(gettext(
'cashbook_media.msg_file_invalid_mime',
recname = line.rec_name,
fmime = line.media_mime,
))
@classmethod
def create(cls, vlist):
""" add media-info
"""
vlist = [x.copy() for x in vlist]
for values in vlist:
if 'media' in values.keys():
values.update(cls.get_media_info(values))
return super(Line, cls).create(vlist)
@classmethod
def write(cls, *args):
""" update media-info
"""
actions = iter(args)
to_write = []
for records, values in zip(actions, actions):
if 'media' in values.keys():
values.update(cls.get_media_info(values))
to_write.extend([
records,
values,
])
super(Line, cls).write(*to_write)
# end Line

15
line.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="line_view_form">
<field name="model">cashbook.line</field>
<field name="inherit" ref="cashbook.line_view_form"/>
<field name="name">line_form</field>
</record>
</data>
</tryton>

44
locale/de.po Normal file
View file

@ -0,0 +1,44 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
##############
# ir.message #
##############
msgctxt "model:ir.message,text:msg_file_too_big"
msgid "The file size of the record '%(recname)s' exceeded the maximum value of 5MB."
msgstr "Die Dateigröße des Datensatzes '%(recname)s' überschreitete den maximalen Wert von 5MB."
msgctxt "model:ir.message,text:msg_file_invalid_mime"
msgid "The file type '%(fmime)s' of the record '%(recname)s' is not allowed. (allowed: PNG, JPG, PDF)"
msgstr "Der Dateityp '%(fmime)s' des Datensatzes '%(recname)s' ist nicht zugelassen. (erlaubt: PNG, JPG, PDF)"
#################
# cashbook.line #
#################
msgctxt "view:cashbook.line:"
msgid "Image/PDF"
msgstr "Bild/PDF"
msgctxt "field:cashbook.line,media:"
msgid "Image of PDF"
msgstr "Bild oder PDF"
msgctxt "field:cashbook.line,media_name:"
msgid "File name"
msgstr "Dateiname"
msgctxt "field:cashbook.line,media_id:"
msgid "File ID"
msgstr "Datei-ID"
msgctxt "field:cashbook.line,media_mime:"
msgid "MIME"
msgstr "MIME"
msgctxt "field:cashbook.line,media_size:"
msgid "File size"
msgstr "Dateigröße"

36
locale/en.po Normal file
View file

@ -0,0 +1,36 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "model:ir.message,text:msg_file_too_big"
msgid "The file size of the record '%(recname)s' exceeded the maximum value of 5MB."
msgstr "The file size of the record '%(recname)s' exceeded the maximum value of 5MB."
msgctxt "model:ir.message,text:msg_file_invalid_mime"
msgid "The file type '%(fmime)s' of the record '%(recname)s' is not allowed. (allowed: PNG, JPG, PDF)"
msgstr "The file type '%(fmime)s' of the record '%(recname)s' is not allowed. (allowed: PNG, JPG, PDF)"
msgctxt "view:cashbook.line:"
msgid "Image/PDF"
msgstr "Image/PDF"
msgctxt "field:cashbook.line,media:"
msgid "Image of PDF"
msgstr "Image of PDF"
msgctxt "field:cashbook.line,media_name:"
msgid "File name"
msgstr "File name"
msgctxt "field:cashbook.line,media_id:"
msgid "File ID"
msgstr "File ID"
msgctxt "field:cashbook.line,media_mime:"
msgid "MIME"
msgstr "MIME"
msgctxt "field:cashbook.line,media_size:"
msgid "File size"
msgstr "File size"

16
message.xml Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.message" id="msg_file_too_big">
<field name="text">The file size of the record '%(recname)s' exceeded the maximum value of 5MB.</field>
</record>
<record model="ir.message" id="msg_file_invalid_mime">
<field name="text">The file type '%(fmime)s' of the record '%(recname)s' is not allowed. (allowed: PNG, JPG, PDF)</field>
</record>
</data>
</tryton>

View file

@ -42,7 +42,7 @@ with open(path.join(here, 'versiondep.txt'), encoding='utf-8') as f:
major_version = 6
minor_version = 0
requires = []
requires = ['python-magic>=0.4.24', 'Pillow']
for dep in info.get('depends', []):
if not re.match(r'(ir|res|webdav)(\W|$)', dep):
if dep in modversion.keys():

24
tests/__init__.py Normal file
View file

@ -0,0 +1,24 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import trytond.tests.test_tryton
import unittest
from trytond.modules.cashbook_media.tests.test_line import LineTestCase
__all__ = ['suite']
class CashbookTestCase(\
LineTestCase,
):
'Test cashbook module'
module = 'cashbook_media'
# end CashbookTestCase
def suite():
suite = trytond.tests.test_tryton.suite()
suite.addTests(unittest.TestLoader().loadTestsFromTestCase(CashbookTestCase))
return suite

5
tests/img_data.py Normal file

File diff suppressed because one or more lines are too long

228
tests/test_line.py Normal file
View file

@ -0,0 +1,228 @@
# -*- coding: utf-8 -*-
# This file is part of the cashbook-module from m-ds for Tryton.
# The COPYRIGHT file at the top level of this repository contains the
# full copyright notices and license terms.
from io import BytesIO
from PIL import Image
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.modules.cashbook.tests import CashbookTestCase
from datetime import date
from decimal import Decimal
from .img_data import img_data_png, dok_data_pdf, text_data
class LineTestCase(CashbookTestCase):
'Test cashbook line module'
module = 'cashbook_media'
@with_transaction()
def test_media_add_image(self):
""" create cook/line, add png-file
"""
pool = Pool()
Book = pool.get('cashbook.book')
Lines = pool.get('cashbook.line')
types = self.prep_type()
category = self.prep_category(cattype='in')
company = self.prep_company()
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
},])],
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.state, 'open')
# add image to line-1
Lines.write(*[
[book.lines[0]],
{
'media': img_data_png,
'media_name': 'image.png',
}])
self.assertEqual(book.lines[0].media_size, 18428)
self.assertEqual(book.lines[0].media_mime, 'image/png')
self.assertEqual(book.lines[0].media_name, 'image.png')
# replace image at line-1 by pdf
Lines.write(*[
[book.lines[0]],
{
'media': dok_data_pdf,
}])
self.assertEqual(book.lines[0].media_size, 8724)
self.assertEqual(book.lines[0].media_mime, 'application/pdf')
self.assertEqual(book.lines[0].media_name, 'image.png')
# create line with pdf
Book.write(*[
[book],
{
'lines': [('create', [{
'date': date(2022, 5, 2),
'description': 'Text 2',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
'media': dok_data_pdf,
'media_name': 'data.pdf',
}])],
}
])
self.assertEqual(len(book.lines), 2)
self.assertEqual(book.lines[1].media_size, 8724)
self.assertEqual(book.lines[1].media_mime, 'application/pdf')
self.assertEqual(book.lines[1].media_name, 'data.pdf')
@with_transaction()
def test_media_add_invalid_file(self):
""" create cook/line, add txt-file
"""
pool = Pool()
Book = pool.get('cashbook.book')
Lines = pool.get('cashbook.line')
types = self.prep_type()
category = self.prep_category(cattype='in')
company = self.prep_company()
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
},])],
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.state, 'open')
# add invalid file
self.assertRaisesRegex(UserError,
"The file type 'text/plain' of the record '05/02/2022|Rev|1.00 usd|Text 2 [Cat1]' is not allowed. (allowed: PNG, JPG, PDF)",
Book.write,
*[
[book],
{
'lines': [('create', [{
'date': date(2022, 5, 2),
'description': 'Text 2',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
'media': text_data,
'media_name': 'text.txt',
}])],
}
])
# replace image at line-1 by invalid file
self.assertRaisesRegex(UserError,
"The file type 'text/plain' of the record '05/02/2022|Rev|1.00 usd|Text 2 [Cat1]' is not allowed. (allowed: PNG, JPG, PDF)",
Lines.write,
*[
[book.lines[0]],
{
'media': text_data,
'media_name': 'text.txt',
},
])
@with_transaction()
def test_media_add_big_file(self):
""" create cook/line, add big png-file
"""
pool = Pool()
Book = pool.get('cashbook.book')
Lines = pool.get('cashbook.line')
types = self.prep_type()
category = self.prep_category(cattype='in')
company = self.prep_company()
party = self.prep_party()
book, = Book.create([{
'name': 'Book 1',
'btype': types.id,
'company': company.id,
'currency': company.currency.id,
'number_sequ': self.prep_sequence().id,
'start_date': date(2022, 5, 1),
'lines': [('create', [{
'date': date(2022, 5, 1),
'description': 'Text 1',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
},])],
}])
self.assertEqual(book.name, 'Book 1')
self.assertEqual(len(book.lines), 1)
self.assertEqual(book.state, 'open')
# construct image
with BytesIO() as fhdl:
img1 = Image.new('RGB', (3200, 1340))
img1.save(fhdl, 'PNG', optimize=True)
del img1
fhdl.seek(0)
img_big_data = fhdl.read()
# create line with png, should be resized
Book.write(*[
[book],
{
'lines': [('create', [{
'date': date(2022, 5, 2),
'description': 'Text 2',
'category': category.id,
'bookingtype': 'in',
'amount': Decimal('1.0'),
'party': party.id,
'media': img_big_data,
'media_name': 'big.png',
}])],
}
])
self.assertEqual(len(book.lines), 2)
self.assertEqual(book.lines[1].media_mime, 'image/jpeg')
self.assertEqual(book.lines[1].media_size, 10221)
self.assertEqual(book.lines[1].media_name, 'big.jpg')
# check image size
with BytesIO(book.lines[1].media) as fhdl:
img2 = Image.open(fhdl, 'r')
self.assertEqual(img2.size, (2000, 837))
# end LineTestCase

View file

@ -3,3 +3,5 @@ version=6.0.0
depends:
cashbook
xml:
message.xml
line.xml

24
view/line_form.xml Normal file
View file

@ -0,0 +1,24 @@
<?xml version="1.0"?>
<!-- This file is part of the cashbook-module from m-ds for Tryton.
The COPYRIGHT file at the top level of this repository contains the
full copyright notices and license terms. -->
<data>
<xpath expr="/form/notebook/page[@name='references']" position="after">
<page name="media" col="4" string="Image/PDF">
<label name="media"/>
<field name="media"/>
<newline/>
<label name="media_name"/>
<field name="media_name"/>
<label name="media_mime"/>
<field name="media_mime"/>
</page>
</xpath>
</data>