Blog | LordVan's Page / Bloghttps://blog.lordvan.com/blog/2024-03-28T19:40:40.648844+00:00If you were looking for something specific you probably got redirected here from an old link to my (now gone) drupal blog. I migrated all the pages & blog entries to this blog, so just use the search here to find what you were looking for.Upgrading mezzanine (forced to due to move to python3.10)2023-06-27T05:55:56+00:002024-03-28T19:40:40.648844+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/upgrading-mezzanine-forced-to-due-to-move-to-python310/<p>A recent system update prompted me to actually remove python3.9 .. which was used for my Django pages (including this one). An issue with a depreciated/removed feature in 3.10 made it needed to not just move from 3.9 -> 3.10 but also to upgrade packages.</p>
<p>Upgrading in-place was something I had tried before and it did not go well, so since my Mezzanine is pretty much vanilla I decided to just try to install the latest one and let it have a go at the DB. Worked a lot better than I had expected. Here's just a quick rundown for future reference (of myself and/or others):</p>
<p>So first save a list of installed packages in the current one with <code>pip freeze</code>, then move the current install to a seperate folder and then start fresh:</p>
<pre>python -mvenv myenv<br/>source myenv/bin/activate<br/>pip install mezzanine psycopg2</pre>
<p>Anything else I only let it install as part of deps to try first, then I went on and created a new mezzanine project with the same name as before:</p>
<pre>mezzanine-project lordvan<br/>cd lordvan<br/>cp old/lordvan/local_settings.py lordvan/local_settings.py<br/>./manage.py migrate</pre>
<p>As a pleasant surprise this just migrated everything out of the box without any issues whatsoever (keep in mind my mezzanine was real old already). Next a test with</p>
<pre>./manage.py runserver <ip:port></pre>
<p>And it was just working. Of course what I had forgotten was that the Mezzanine start page, so next I just copied the new templates and edited them:</p>
<pre>mkdir templates<br/>cd templates<br/>cp myenv/lib64/python3.10/site-packages/mezzanine/core/templates/base.html .<br/>cp myenv/lib64/python3.10/site-packages/mezzanine/core/templates/index.html .</pre>
<p>Now I had my page back up and running like before. Of course I forgot the usual again which caused a HTTP error in the browser:</p>
<pre>cp old/lordvan/apache.wsgi lordvan/<br/>chgrp apache lordvan -R<br/># edit apache config if you hardcoded the pythonpath there<br/>/etc/init.d/apache2 restart</pre>
<p>now it worked but there was no CSS/.. of course I also (again) forgot something else:</p>
<pre>./manage.py collectstatic</pre>
<p>and now we are up and running again .. except for one little thing: I had an error while saving this blog post .. (which caused me to type it again): <code>unsupported operand type(s) for +: 'frozenset' and 'list'</code></p>
<p>After looking it up and checking the source I just edited the relevant mezzanine file (<code>myenv/lib64/python3.10/site-packages/mezzanine/utils/html.py</code>) and changed line 113 to this:</p>
<pre> protocols=list(ALLOWED_PROTOCOLS) + ["tel"],</pre>
<p>now it runs without a problem. (there is also a bug <a href="https://github.com/stephenmcd/mezzanine/pull/2059">https://github.com/stephenmcd/mezzanine/pull/2059</a> not sure about the 2nd part there . mine works as-is now so I'm going with that for now) - some people also simply downgraded Bleach to < 6.</p>
<p>Just for the record here the versions before and after:</p>
<ul>
<li>python 3.9.16 => 3.10.12</li>
<li>Django 1.10.8 => 4.2.2</li>
<li>Mezzanine 4.2.3 => 6.0.0</li>
<li>psycopg2 2.8.6 => 2.9.6</li>
<li>rest just up to date at time of install</li>
</ul>[Tryton] Adding a default account(ing) category to product (templates)2021-06-13T17:25:57.519843+00:002024-03-28T00:48:55.686920+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/tryton-adding-a-default-accounting-category-to-product-templates/<p>To optimize workflow (and avoid mistakes) I wanted to set a default account(ing) category so that tax,.. is set by default. Took a little bit of experimenting, but I got it:</p>
<pre>class Template(metaclass=PoolMeta):<br/> __name__ = 'product.template'<br/><br/> @classmethod<br/> def default_account_category(cls):<br/> try:<br/> return Pool().get('product.category').search([('name', '=', 'MWSt'), ('accounting', '=', True)])[0].id<br/> except:<br/> # FIXME: should I log an error here for not finding a hardcoded accounting category? <br/> return None<br/><br/></pre>
<p>I could have of course also returned the number for the tax rate I am usign as well, but I found this to be (a lot) more flexible should I ever want to make it user/admin configurable and not hardcode it. But for now this is more than fine.</p>[Tryton] product kit .. extra logic for components when expanded on sale quotation2021-06-13T09:53:10+00:002024-03-28T06:51:44.356750+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/tryton-product-kit-extra-logic-for-components-when-expanded-on-sale-quotation/<p>Since Tryton 6.0 had the awesome <a href="https://discuss.tryton.org/t/product-kit-support/1938" target="_blank">product kit</a> module added I saved a lot of time and only had to add some custom logic for my extra fields.</p>
<p>After some discussion on the IRC channel I had a vague idea of what to do (write my own get_components was up for discussion,..)</p>
<p>In the end I found a really simple way to do what I wanted after playing around in proteus a bit and inspecting the data. Basically I am making use of <prefix>line.component_children.</p>
<p>Since the components of a kit are expanded on quotation I just added one more method to my customized Sale class:</p>
<pre> @classmethod<br/> @ModelView.button<br/> @Workflow.transition('quotation')<br/> @set_employee('quoted_by')<br/> def quote(cls, sales):<br/> super(Sale, cls).quote(sales)<br/> # we need to apply folder_no to components of kits<br/> for sale in sales:<br/> for line in sale.lines:<br/> for lc in line.component_children:<br/> lc.folder_no = line.folder_no<br/> lc.save()<br/> cls.save(sales)</pre>
<p>I am not 100% sure if the last cls.save(sales) is needed, but I thought rather be safe than sorry. Now my components have the same folder_no as the kits which is needed for my project documentation.</p>Migrated mezzanine to the new webserver ..2021-05-24T21:12:56+00:002024-03-28T06:47:03.539689+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/migrated-mezzanine-to-the-new-webserver/<p>This is more of a test post, but also keep in mind when upgrading python versions to also remove version numbers on some of the requirements.txt stuff ..</p>
<p>in particular psycopg, Pillow .. I just ran <code>pip install Django==<version number> Mezzanine==<version number> Pillow psycopg2</code> . That worked for me now the django and mezzanine upgrade will have to wait for another day.</p>
<p>Also had some rather strange error <code>RuntimeError: __class__ not set defining 'AbstractBaseUser' as <class 'django.contrib.auth.base_user.AbstractBaseUser</code> .. which seemed obscure and I did not know where to start .. fortunately I was not the only one (and definitely not the first one) who got this. Found the solution at <a href="https://stackoverflow.com/questions/61711710/runtimeerror-class-not-set-defining-abstractbaseuser-as-class-django-co" target="_blank">stackoverflow</a>. Adding those few lines of code fixed it for me. (just in case the page goes offline/changes here the code copied (<code>myenv/lib64/python3.9/site-packages/django/db/models/base.py</code>):</p>
<pre class="lang-py s-code-block hljs python"><code><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ModelBase</span>(<span class="hljs-params"><span class="hljs-built_in">type</span></span>):</span>
<span class="hljs-string">"""
Metaclass for all models.
"""</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__new__</span>(<span class="hljs-params">cls, name, bases, attrs</span>):</span>
super_new = <span class="hljs-built_in">super</span>(ModelBase, cls).__new__
<span class="hljs-comment"># Also ensure initialization is only performed for subclasses of Model</span>
<span class="hljs-comment"># (excluding Model class itself).</span>
parents = [b <span class="hljs-keyword">for</span> b <span class="hljs-keyword">in</span> bases <span class="hljs-keyword">if</span> <span class="hljs-built_in">isinstance</span>(b, ModelBase)]
<span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> parents:
<span class="hljs-keyword">return</span> super_new(cls, name, bases, attrs)
<span class="hljs-comment"># Create the class.</span>
module = attrs.pop(<span class="hljs-string">'__module__'</span>)
new_class = super_new(cls, name, bases, {<span class="hljs-string">'__module__'</span>: module})
<span class="hljs-comment"># <========== THE CODE BELLOW SHOULD BE ADDED ONLY ======>></span>
new_attrs = {<span class="hljs-string">'__module__'</span>: module}
classcell = attrs.pop(<span class="hljs-string">'__classcell__'</span>, <span class="hljs-literal">None</span>)
<span class="hljs-keyword">if</span> classcell <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:
new_attrs[<span class="hljs-string">'__classcell__'</span>] = classcell
new_class = super_new(cls, name, bases, new_attrs)
<span class="hljs-comment"># <========== THE CODE ABOVE SHOULD BE ADDED ONLY ======>></span>
<span class="hljs-comment"># the rest of the class .....</span>
</code></pre>
<p>.</p>Tryton upgrade from 5.8 to 6.02021-05-23T19:03:45+00:002024-03-28T10:27:07.855290+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/tryton-upgrade-vrom-58-to-60/<p>Upgrading Tryton from 5.8 to 6.0 was nearly completely painless - in part due to the fact I am only using a few modules so far (and my custom module is not too big either).</p>
<p>Following the <a href="https://discuss.tryton.org/t/migration-from-5-8-to-6-0/3603">Migration post</a> worked out just fine.</p>
<p>Here are the few things I had to change:</p>
<p>1) I was told beforehand on IRC by pokoli that the Report method get_context had an extra parameter <code>header</code> so I checked the source and added that (<a href="https://github.com/LordVan/tryton-modules/commit/3c7bf2d83c97644eee8053301695d5cfad42d6f4" target="_blank">commit on my github repo</a>).</p>
<p>2) The address field zip got renamed to the more generic postal_code (which is <a href="https://discuss.tryton.org/t/tryton-release-6-0/4094" target="_blank">mentioned here</a>) so I had to change the report.</p>
<p>(I also had to add correct DB URI escape syntax to my trytond config but that is not something everyone will have depending on the DB used, passwords,,..)</p>
<p>Now on to use the new product_kit module ... :)</p>
<p></p>Tryton, form state change with Eval not working with custom field (+ fix)2021-05-14T18:56:40.971095+00:002024-03-28T01:14:21.696846+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/tryton-form-state-change-with-eval-not-working-how-to-get-it-to-work/<p>I added a Boolean field to SaleLine - one that is not shown to the user but only set in the code as a flag. As the readonly state of just about all my custom fields should depend on that I added:</p>
<pre>states = {'readonly': ((Eval('sale_state') != 'draft') |
Eval('real_product')),
}, </pre>
<p>Frustratingly it did not work even though the rest of my code was executed in <code>on_change_product</code> as was seen on the assignment of values.</p>
<p>Took me a while to figure out what the problem was .. apparently for <code>Eval('real_product')</code> to work I needed to add this field to my <code>sale_line_form.xml</code>. So I did just that and to not show it to the user of course added <code>states = { 'invisible': True, }</code> to my Boolean field. Simple when you know it .. but takes a while to figure out if you don't.</p>Dolibarr -> Tryton data import finally done2021-04-25T19:01:31.777098+00:002024-03-28T12:13:13.733769+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/dolibarr-tryton-data-import-finally-done/<p>So I have finally imported all data from our old dolibarr install to tryton (the historical project data was missing - and because I needed to implement something in tryton first it had to wait a bit)</p>
<p>It's actually fairly simple. Due to the nature of the data (historical, not in-sequence with current projects,..) I had to use trytond-console instead of proteus but there are only a few differences for small imports like this (like pool.get(<model>) instead of Model.get (<model>) and of course it does not do all the checks proteus and the client do (which is good cuz otherwise it would be near impossible to import historical data this easily) ..</p>
<p>Anyway the code is on my <a href="https://github.com/LordVan/tryton-modules/tree/master/dolibarr_import" target="_blank">github</a>:</p>
<p></p>Small script to find duplicate contact_mechanisms for parties in tryton2021-01-06T14:02:46+00:002024-03-28T00:51:43.596566+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/small-script-to-find-duplicate-contact_mechanisms-for-parties-in-tryton/<p>Just a little script to find duplicate contact_mechanism entries on parties in tryton <a href="https://blog.lordvan.com/blog/ipython-loading-py-files-to-make-interactive-use-of-tryton-faster/">(how i run tryton stuff using ipython and proteus)</a>:</p>
<pre> for p in Party.find():
cms = p.contact_mechanisms
if cms:
nos = []
for cm in cms:
if f'{cm.type} {cm.value_compact}' in nos:
print(f'duplicate contact mechanism {p.code=} {p.name=} {cm.value} {cm.type}')
else:
nos.append(f'{cm.type} {cm.value_compact}')
</pre>
<p>I just ran this from ipython to find out if there are a lot of duplicates - turns out i had barely anything (and i fixed that before re-running the imports for good in the old system).</p>fun little (import) progress display (command line) in python2021-01-05T10:38:01.675312+00:002024-03-28T00:47:18.720221+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/fun-little-import-progress-display-command-line-in-python/<p>So I wanted some sort of progress printed while my import into tryton runs .. I just had dots to start with .. not easy to keep track though.</p>
<p>Came up with this fun little function to make it easier to see approximately how far (in a ASCII-graphical way:</p>
<pre>def print_progress(ncount):
scount = ' '
if ncount % 1000 == 0:
scount = 'M'
elif ncount % 500 == 0:
scount = 'D'
elif ncount % 100 == 0:
scount = 'C'
elif ncount % 50 == 0:
scount = 'L'
elif ncount % 10 == 0:
scount = 'X'
else:
scount = '.'
print(scount, end='', flush = True)
</pre>
<p>use dots in general, but dump in some roman literals to make it easier</p>ipython loading .py files to make interactive use of tryton faster2021-01-04T09:40:34.526107+00:002024-03-27T10:34:03.583538+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/ipython-loading-py-files-to-make-interactive-use-of-tryton-faster/<p>So I was busy testing tryton import scripts .. which means constantly re-setting my DB to clear it (deleting just takes 10 times longer than SQL restore)..</p>
<p>But since I drop and recreate the DB the ipython session also needs to disconnect... which is a bit inconvenient because of setup,..</p>
<p>Fortunately ipython has <code>%load</code> - which makes this super easy:</p>
<p><img alt="Screenshot of script" height="519" src="https://blog.lordvan.com/static/media/uploads/Blog/Screenshots/shot-2021-01-04_10-24-27.jpg" width="978"/></p>
<p>(screenshot to show off just how nicely everything is even highlighted too)</p>
<p>now here the code to copy paste if someone wants it:</p>
<pre>import myinit
from proteus import config, Model, Wizard, Report
pcfg = config.set_trytond(database='tryton', config_file='/etc/tryton/trytond.conf')
Party = Model.get('party.party')
Addr = Model.get('party.address')
Cont = Model.get('party.contact_mechanism')
Note = Model.get('ir.note')
Lang = Model.get('ir.lang')
Categ = Model.get('party.category')
Country = Model.get('country.country')
SubD = Model.get('country.subdivision')
Cont = Model.get('party.contact_mechanism')
Ident = Model.get('party.identifier')
try:
prm, = Party.find([('code', '=', '1')])
except:
print('Could not load party with code 1')
</pre>
<p>myinit just contains the code to hide warnings from <a href="https://stackoverflow.com/questions/9031783/hide-all-warnings-in-ipython">this stackoverflow question</a></p>
<p>This is super useful in setting up the basic types I need for my testing.</p>Tryton Proteus adding Notes2021-01-03T15:45:57.272322+00:002024-03-28T06:45:02.649940+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/tryton-proteus-adding-notes/<p>So after a bit of experimenting I now know how to add Notes to other objects (like Party) for example:</p>
<p>(Assuming you got the connection set up in ipython using proteus or doign this in some script)</p>
<pre>Note = Model.get('ir.note')<br/>n2=Note()<br/>n2.message = 'this is a test note from proteus'<br/>n2.resource = prm # prm is a Party object that I fetched before<br/>n2.save()</pre>
<p>Fairly simple. not sure yet how to get notes that "belong" to a party object, but that is not important right now since I only need it for importing ;)</p>python virtualenv are awesome ;) [certbot]2020-05-25T18:11:10+00:002024-03-28T11:55:06.382233+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/python-virtualenv-are-awesome-certbot/<p>So I was having a small issue on a server I cannot easily update where certbot was out of date and gave me this error: "Client with the currently selected authenticator does not support any combination of challenges that will satisfy the CA."<br/>Took me a while due to some dependencies my last updates had skipped certbot and it was still quite old .. Digging around a bit I found out that the error I had might be fixed already. But without a good way to update it I was a bit stuck..<br/>Then I remembered .. who said I had to use the system installed certbot.. so virtualenv to the rescue:</p>
<p>python3.7 -mvenv myenv<br/>source myenv/bin/activate<br/>pip install certbot</p>
<p>and then just run that certbot to renew my (dns - challenge, wildcard) certificate :) would've been a pain without virtualenv for sure</p>Python network "scanner"2020-05-01T18:37:21.204681+00:002024-03-26T12:50:22.239587+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/python-network-scanner/<p>So i wanted to know what hosts in my network are up and what are their mac addresses.</p>
<p>nmap -sn does a good job, but the output is not the best to parse imho .. and also mac addresses are only shown to root..</p>
<p>So i looked at python3-nmap and wrote a little script to do that .. of course it still won't show the MAC addresses there for non root .</p>
<p>Too lazy to call "arp" and then parse the output i looked for an alternative and found the arpreq module which queries the kernel apr table - and since just before i scanned for hosts that should have everything. -- Also tried with SCAPY, but same problem .. only for root will it show that ..(i am aware as to why but don't really want to go into that now)</p>
<p>Added reverse dns lookup too afterwards.</p>
<p>Maybe someone else will need this or find some part of it useful:</p>
<pre>#!/usr/bin/env python3
import re
import sys
import nmap3
from dns import reversename, resolver # from dnspython package
USE_SCAPY = False # only works as root ..
if USE_SCAPY:
from scapy import all as sa
else:
import arpreq
DEBUG = False
DEBUG2 = False # includes dir(..)
class HostInfo:
_ip = None
_mac = None
_dns = None
def __init__(self, ip=None, mac=None, dns=None):
self.ip = ip
self.mac = mac
self.dns = dns
def __str__(self):
return 'IP: %s - MAC: %s - DNS: %s' % (self.ip, self.mac, self.dns)
@property
def ip(self):
return self._ip
@ip.setter
def ip(self, value):
self._ip = value
@property
def mac(self):
return self._mac
@mac.setter
def mac(self, value):
self._mac = value
@property
def dns(self):
return self._dns
@dns.setter
def dns(self, value):
self._dns = value
class NetworkScanner:
_n = None
_sa = None
_nm_hosts = []
_hostlist = []
def __init__(self):
self._n = nmap3.Nmap()
if DEBUG2:
print(dir(self._n))
def find_hosts(self, subnet, scan_type="-sn"):
self._nm_hosts = self._n.nmap_list_scan(subnet, scan_type)
for host in self._nm_hosts:
h = HostInfo(host['addr'])
if DEBUG:
print(host)
if USE_SCAPY:
h.mac = sa.getmacbyip(host['addr']) # only works as root ..
else:
# use aprreq to query the kernel arp table since it should have it anyway
h.mac = arpreq.arpreq(host['addr'])
self._hostlist.append(h)
rev_name = reversename.from_address(h.ip)
try:
h.dns = str(resolver.query(rev_name, "PTR")[0])
except Exception as e:
if DEBUG:
print(e)
return self._nm_hosts, self._hostlist
if __name__ == '__main__':
ns = NetworkScanner()
subnet = input('Enter subnet name (CIDR notation): ')
cidr_pat = re.compile("^([0-9]{1,3}\.){3}[0-9]{1,3}(/([0-9]|[1-2][0-9]|3[0-2]))?$")
if cidr_pat.match(subnet):
print('Querying subnet {0}'.format(subnet))
else:
print('Invalid Subnet specification')
sys.exit(1)
hosts, hl = ns.find_hosts(subnet)
for hi in hl:
print(hi)
if DEBUG2:
print(dir(sa))
</pre>
<p>The next part will be to generate my (part of) DHCP config for fixed addresses from this .. or at least a template for it since I intend to use it with DNS of course</p>Quodlibet .. an even more awesome music player than I had thought :)2020-04-24T19:08:19+00:002024-03-26T08:38:54.344650+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/quodlibet-an-even-more-awesome-music-player-than-i-had-thought/<p>So I was getting annoyed with my music player as I was having the "problem" of having my songs in ogg, mp3 and flac usually (i like to rip in those 3 formats for various reasons). But of course with 3 formats of the same songs in each directory every player that can play all of them will show each song 3 times .. which is very annoying if you just want to play an album .. so I thought about how to do this and first tried to just use the ratings to rate all of the .ogg higher or soo.. works but is super annoying..</p>
<p>Then I just <a href="https://quodlibet.readthedocs.io/en/latest/guide/searching.html">read up</a> on something and found out that <a href="https://github.com/quodlibet/">quodlibet</a> is more awesome than I thought .. i was under the impression it could only do searches based on regex instead of just plain old searches .. but it can do so much more! reading up on it I found out that you can even use searches or regex to filter on individual tags (including internal tags like filename and format !!). so all I need to do is filter to only show one format (in this case probably mp3 most of the time since some older rips are not yet done in all 3 formats).</p>
<p><a href="https://blog.lordvan.com/static/media/uploads/Blog/Screenshots/quodlibet_search_and_preferences.png" target="_blank"><img alt="Quod Libet Screenshot (description below)" height="323" src="https://blog.lordvan.com/static/media/uploads/Blog/Screenshots/quodlibet_search_and_preferences.png" width="615"/></a></p>
<p>In the screenshots you can see 2 things:</p>
<ol>
<li>my search / filter for filename ending in flac and artist containing Five Finger</li>
<li>my preferences with my own little custom column that shows me the format - which i plan to use instead of filename based (since I just now figured out that format is there too) - which is why i wanted to show it so i know what to match</li>
</ol>
<p>It occurs to me that it should be possible with either one of the existing plugins or by writing a new one to filter songs so that each song is only there once no matter which file types are available .. something like check if a song is available as flac, ogg, mp3 and choose the first one that is found. - that way all 3 types would be shown if there is only an ogg or mp3 for one.. will probalby look into that since the whole thiing is python :D</p>some PyQt5 ..2020-03-20T13:16:22.127014+00:002024-03-26T21:54:23.351466+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/some-pyqt5/<p>So i wanted a (very simple) GUI for my python program .. and i want it to run on Linux but also windows..</p>
<p>Looking at the choices (Kivy, PyQt, pygtk) I found PyQt to be the most complete toolkit (I want a treeview with more columns later on which is something that kivy doesn'T seem to have). - PyGTK has that too but under windows that is a bit more of a pain to get running (from prior experience).</p>
<p>So I went and tried PyQt5 for the first time (last time I used PyQt it was Version 4 still so been a while ..)</p>
<p>It's fairly easy to get running with PYCharm (Community Edition) just go to the project settings and install the PyQt5 module -- don'T forget the stubs (for some reason installing from the popup hint did not work for me so I did it manually).</p>
<p>I installed qtdesigner too, but couldn'T figure out how to best use it with pycharm immediately so I just put together my (very simple) GUI the old way for now.</p>
<p>The purpose is simple:</p>
<ol>
<li>pick a base directory</li>
<li>pick a text file with a list of filenames [also picks the file'S directory as source)</li>
<li>verify it makes sense (the text file and source directory need to be within the base directory</li>
<li>[[ This is in a seperate module and has been used from the command line before ]] find all files from the text file in the base directory, compare to the source one and then save the result (and output it to a textfile and / or QPlainTextEdit later .. maybe will add TreeView at some point but not right now)</li>
</ol>
<p>so just for anyone wanting to just get a basic idea of how to do this here some source code without much explaination (fairly self explainatory if you are familiar with python and GUI toolkits imho):</p>
<pre>import sys
# from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QLabel, QWidget, QPushButton, QGridLayout, QLineEdit, QFileDialog,
QPlainTextEdit, QTreeWidget) # QErrorMessage)
from file_find_compare import FileFindCompare
DEBUG = False
class PyFindFileCompare(QWidget):
# widgets
base_dir = None
inp_file = None
out_text = None
btn_base_dir = None
btn_inp_file = None
btn_start = None
tv_out = None
_out_file = None
def __init__(self):
super().__init__()
self.init_ui()
def init_ui(self):
layout = QGridLayout()
layout.addWidget(QLabel(text='Basisordner: '), 0, 0)
self.base_dir = QLineEdit()
self.base_dir.setReadOnly(True)
self.base_dir.setText('//server/Daten')
layout.addWidget(self.base_dir, 0, 1)
self.btn_base_dir = QPushButton('Basisordner auswählen: ')
layout.addWidget(self.btn_base_dir, 0, 2)
self.btn_base_dir.clicked.connect(self.get_basedir)
layout.addWidget(QLabel(text='Eingabedatei: '), 1, 0)
self.inp_file = QLineEdit()
self.inp_file.setReadOnly(True)
self.inp_file.setText('//server/Daten')
layout.addWidget(self.inp_file, 1, 1)
self.btn_inp_file = QPushButton('Eingabedatei auswählen')
self.btn_inp_file.setDisabled(True)
layout.addWidget(self.btn_inp_file, 1, 2)
self.btn_inp_file.clicked.connect(self.get_inp_file)
self.btn_start = QPushButton('Start')
self.btn_start.setDisabled(True)
layout.addWidget(self.btn_start, 2, 2)
self.btn_start.clicked.connect(self.start_search)
self.out_text = QPlainTextEdit()
self.out_text.setReadOnly(True)
layout.addWidget(self.out_text, 3, 0, 1, 3)
self.tv_out = QTreeWidget()
self.tv_out.hide()
layout.addWidget(self.tv_out, 4, 0, 1, 3)
self.setLayout(layout)
self.setFixedWidth(900)
self.setMinimumHeight(500)
self.show()
def get_basedir(self):
bdir = str(QFileDialog.getExistingDirectory(self,
"Basisordner auswählen",
self.base_dir.text()
))
if bdir:
self.base_dir.setText(bdir)
# we got a valid input path so enable the input file button
self.btn_inp_file.setDisabled(False)
if DEBUG:
self.out_text.appendPlainText('got base dir: %s' % self.base_dir.text())
if self.inp_file.text() != '':
self.inp_file.setText(self.base_dir.text())
def get_inp_file(self):
# stop the user from changing the base dir to avoid weird issues
# TODO: make it more user friendly later
self.btn_base_dir.setDisabled(True)
infile = str(QFileDialog.getOpenFileName(self,
"Eingabedatei auswählen",
self.inp_file.text(),
'*.txt'
)[0]
)
if infile:
if infile.find(self.base_dir.text()) != 0:
self.out_text.appendPlainText('Fehler: Die Eingabedatei muß im Basisverzeichnis oder unterordner liegen!')
return
else:
self.btn_start.setDisabled(False)
self.inp_file.setText(infile)
self._out_file = self.inp_file.text().replace('.txt', '_output.txt')
if DEBUG:
self.out_text.appendPlainText('got input file: %s' % self.inp_file.text())
self.out_text.appendPlainText('output filename: %s' % self._out_file)
def start_search(self):
# just to be save disable both other buttons
self.btn_inp_file.setDisabled(True)
self.btn_inp_file.setDisabled(True)
ffc = FileFindCompare(self.inp_file.text(), self._out_file, self.base_dir.text())
self.out_text.appendPlainText('Liste der Dateien: %s' % self.inp_file.text())
self.out_text.appendPlainText('Basisordner: %s' % self.base_dir.text())
ffc.find_compare()
self.out_text.appendPlainText('Ausgabe wird gespeichert als %s' % self._out_file)
self.out_text.appendPlainText('Ausgabe als text:')
self.out_text.appendPlainText(ffc.generate_output())
ffc.save_output()
def populate_tv(self):
# TODO: populate the treeview
self.tv_out.show()
if __name__ == '__main__':
app = QApplication([])
win = PyFindFileCompare()
sys.exit(app.exec_())
</pre>
<p>I made some assumptions / simpilifications for the sake of usability / userfriendliness for now since I was in a rush .. so I am aware there are some issues / not very pretty things but it works and for now that was what mattered -- and to be honest it is such a short / simple program that I am going to be using myself for like 80% of the time if not 100% so whatever ^^ </p>on getting party (company and people) data from dolibarr to import into tryton ..2019-02-01T17:06:54.151436+00:002024-03-28T15:45:30.589666+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/on-getting-party-company-and-people-data-from-dolibarr-to-import-into-tryton/<p>I tried with just exporting from postgresql like this:</p>
<pre>COPY (SELECT ROW_TO_JSON(t)<br/>FROM (SELECT * FROM llx_societe) t) to '/path/to/file/llx_societe_extrafields.json';</pre>
<p>but that gives me so much that I do not need and also still keeps the half-french colum names (which as someone who doesn't speak french is driving me mad and slowing me down..)</p>
<p>Warning: Postgresql does not seem to escape " from HTML so you need to escape it or remove it (which is hwat i did since I do not need it)</p>
<p>so I'll just make query and/or view to deal with this:</p>
<pre>SELECT s.rowid AS s_row,<br/> s.nom AS s_name,<br/> s.phone AS s_phone,<br/> s.fax AS s_fax,<br/> s.email AS s_email,<br/> s.url AS s_url,<br/> s.fax AS s_fax,<br/> s.address AS s_address,<br/> s.town AS s_town,<br/> s.zip AS s_zip,<br/> s.note_public AS s_note_public,<br/> s.note_private AS s_note_private,<br/> s.ape AS s_fbno,<br/> s.idprof4 AS s_dvrno,<br/> s.tva_assuj AS s_UST,<br/> s.tva_intra AS s_uid,<br/> s.code_client AS s_code_client,<br/> s.name_alias AS s_name_alias,<br/> s.siren AS s_siren,<br/> s.siret AS s_siret,<br/> s.client AS s_client,<br/> s_dep.nom AS s_county,<br/> s_reg.nom AS s_country,<br/> s.fk_forme_juridique,<br/> se.pn_name AS s_pn_name,<br/> sp.rowid AS sp_rowid,<br/> sp.lastname AS sp_lastname,<br/> sp.firstname AS sp_firstname,<br/> sp.address as sp_address,<br/> sp.civility AS sp_civility,<br/> sp.address AS sp_address,<br/> sp.zip AS sp_zip,<br/> sp.town AS sp_town,<br/> sp_dep.nom AS sp_county,<br/> sp_reg.nom AS sp_country,<br/> sp.fk_pays AS sp_fk_pays,<br/> sp.birthday AS sp_birthday,<br/> sp.poste AS sp_poste,<br/> sp.phone AS sp_phone,<br/> sp.phone_perso AS sp_phone_perso,<br/> sp.phone_mobile AS sp_phone_mobile,<br/> sp.fax AS sp_fax,<br/> sp.email AS sp_email,<br/> sp.priv AS sp_priv,<br/> sp.note_private AS sp_note_private,<br/> sp.note_public AS sp_note_public<br/> <br/><br/>FROM llx_societe AS s<br/>INNER JOIN llx_societe_extrafields AS se ON se.fk_object = s.rowid<br/>LEFT JOIN llx_socpeople AS sp ON sp.fk_soc = s.rowid<br/>LEFT JOIN llx_c_departements AS s_dep ON s.fk_departement = s_dep.rowid<br/>LEFT JOIN llx_c_regions AS s_reg ON s_dep.fk_region = s_reg.rowid<br/>LEFT JOIN llx_c_departements AS sp_dep ON sp.fk_departement = sp_dep.rowid<br/>LEFT JOIN llx_c_regions AS sp_reg ON sp_dep.fk_region = sp_reg.rowid<br/>ORDER BY s_name, sp_lastname, sp_firstname;</pre>
<p></p>Using proteus to browse / edit / .. tryton's data2019-01-05T15:28:29+00:002024-03-28T15:03:47.058062+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/using-proteus-to-browse-edit-trytons-data/<p>Going to be using this blog post to add bits and pieces of how to use proteus to handle data in tryton.</p>
<p>Just noticed the proteus readme is quite good: here's a link to the <a href="https://github.com/tryton/proteus">proteus github</a></p>
<p><strong>IMPORTANT</strong>: One thing I noticed (the hard way) is that if you are connected with a proteus session and you add&activate a module (at least when it is not done using proteus) you need to re-connect as it does not seem to add things like extra fields added to models otherwise.</p>
<p>First thing: connect:</p>
<pre>from proteus import config, Model, Wizard, Report<br/>pcfg = config.set_trytond(database='trytond', config_file='/etc/tryton/trytond.conf')</pre>
<p>Then we just get ourselved our parties:</p>
<pre>Party = Model.get('party.party')
all_parties=Party.find()
for p in all_parties:
print(p.name)
print(p.addresses[0].full_address
)</pre>
<p>This will print out all names and the first full address of each.</p>
<p>Party Relations (a seperate module):</p>
<pre>p.relations</pre>
<p>Would give you output similar to this (if there are relations - in my case 2):</p>
<pre>[proteus.Model.get('party.relation.all')(2),<br/> proteus.Model.get('party.relation.all')(4)]</pre>
<p>Interesting fields there (for me):</p>
<pre>p.relations[0].type.name # returns the name of the relation as entered<br/>p.relations[0].reverse # reverse relation as entered<br/># the next 2 are self explainatory anyway just note the '_' with from <br/>p.relations[0].to <br/>p.relations[0].from_</pre>
<p>Now to add a new one:</p>
<pre>np = Party()<br/>np.name='Test Customer from Proteus'<br/>np.save()</pre>
<p>This just creates a new party with just a name. default values that are set up (like default language) are set. Until it is saved the <code>id</code> (<code>np.id</code>) is -1. By default it also comes with one (empty address).</p>
<p>Here's how to edit/add:</p>
<pre>np.addresses[0].zip='1234'<br/>np.addresses.new(zip='2345')<br/>np.save() # don't forget this</pre>
<p>Extra fields from other (possibly own) can be accessed exactly the same way as the normal ones (just don't forget to reconnect - like i did ;) )</p>
<p>Here's how you refresh the data:</p>
<pre>np.reload()</pre>
<p>d</p>
<p></p>tryton -- ipython, proteus2018-10-14T09:22:51.407965+00:002024-03-26T12:50:20.487986+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/tryton-ipython-proteus/<pre>So after being told on IRC that you can use (i)python and proteus to poke around a running tryton instance(thanks for that hint btw) I tried it and had some "fun" right away:</pre>
<pre>from proteus import config,Model
pcfg = config.set_trytond(database='trytond', config_file='/etc/tryon/trytond.conf')</pre>
<p>gave me this:</p>
<pre>---------------------------------------------------------------------------<br/>ValueError Traceback (most recent call last)<br/>/usr/lib64/python3.5/site-packages/trytond/backend/__init__.py in get(prop)<br/> 31 ep, = pkg_resources.iter_entry_points(<br/>---> 32 'trytond.backend', db_type)<br/> 33 except ValueError:<br/><br/>ValueError: not enough values to unpack (expected 1, got 0)<br/><br/>During handling of the above exception, another exception occurred:<br/><br/>ImportError Traceback (most recent call last)<br/><ipython-input-2-300353cf02f5> in <module>()<br/>----> 1 pcfg = config.set_trytond(database='trytond', config_file='/etc/tryon/trytond.conf')<br/><br/>/usr/lib64/python3.5/site-packages/proteus/config.py in set_trytond(database, user, config_file)<br/> 281 config_file=None):<br/> 282 'Set trytond package as backend'<br/>--> 283 _CONFIG.current = TrytondConfig(database, user, config_file=config_file)<br/> 284 return _CONFIG.current<br/> 285<br/><br/>/usr/lib64/python3.5/site-packages/proteus/config.py in __init__(self, database, user, config_file)<br/> 232 self.config_file = config_file<br/> 233<br/>--> 234 Pool.start()<br/> 235 self.pool = Pool(database_name)<br/> 236 self.pool.init()<br/><br/>/usr/lib64/python3.5/site-packages/trytond/pool.py in start(cls)<br/> 100 for classes in Pool.classes.values():<br/> 101 classes.clear()<br/>--> 102 register_classes()<br/> 103 cls._started = True<br/> 104<br/><br/>/usr/lib64/python3.5/site-packages/trytond/modules/__init__.py in register_classes()<br/> 339 Import modules to register the classes in the Pool<br/> 340 '''<br/>--> 341 import trytond.ir<br/> 342 trytond.ir.register()<br/> 343 import trytond.res<br/><br/>/usr/lib64/python3.5/site-packages/trytond/ir/__init__.py in <module>()<br/> 2 # this repository contains the full copyright notices and license terms.<br/> 3 from ..pool import Pool<br/>----> 4 from .configuration import *<br/> 5 from .translation import *<br/> 6 from .sequence import *<br/><br/>/usr/lib64/python3.5/site-packages/trytond/ir/configuration.py in <module>()<br/> 1 # This file is part of Tryton. The COPYRIGHT file at the top level of<br/> 2 # this repository contains the full copyright notices and license terms.<br/>----> 3 from ..model import ModelSQL, ModelSingleton, fields<br/> 4 from ..cache import Cache<br/> 5 from ..config import config<br/><br/>/usr/lib64/python3.5/site-packages/trytond/model/__init__.py in <module>()<br/> 1 # This file is part of Tryton. The COPYRIGHT file at the top level of<br/> 2 # this repository contains the full copyright notices and license terms.<br/>----> 3 from .model import Model<br/> 4 from .modelview import ModelView<br/> 5 from .modelstorage import ModelStorage, EvalEnvironment<br/><br/>/usr/lib64/python3.5/site-packages/trytond/model/model.py in <module>()<br/> 6 from functools import total_ordering<br/> 7<br/>----> 8 from trytond.model import fields<br/> 9 from trytond.error import WarningErrorMixin<br/> 10 from trytond.pool import Pool, PoolBase<br/><br/>/usr/lib64/python3.5/site-packages/trytond/model/fields/__init__.py in <module>()<br/> 2 # this repository contains the full copyright notices and license terms.<br/> 3<br/>----> 4 from .field import *<br/> 5 from .boolean import *<br/> 6 from .integer import *<br/><br/>/usr/lib64/python3.5/site-packages/trytond/model/fields/field.py in <module>()<br/> 18 from ...rpc import RPC<br/> 19<br/>---> 20 Database = backend.get('Database')<br/> 21<br/> 22<br/><br/>/usr/lib64/python3.5/site-packages/trytond/backend/__init__.py in get(prop)<br/> 32 'trytond.backend', db_type)<br/> 33 except ValueError:<br/>---> 34 raise exception<br/> 35 mod_path = os.path.join(ep.dist.location,<br/> 36 *ep.module_name.split('.')[:-1])<br/><br/>/usr/lib64/python3.5/site-packages/trytond/backend/__init__.py in get(prop)<br/> 24 if modname not in sys.modules:<br/> 25 try:<br/>---> 26 __import__(modname)<br/> 27 except ImportError as exception:<br/> 28 if not pkg_resources:<br/><br/>ImportError: No module named 'trytond.backend.'</pre>
<p>Took me a while to figure out I just had a typon in the config file path. Since that cost me some time I thought I'd put it on here so that maybe someone else who makes the same mistake doesn't waste as much time on it as me ;) -- and thanks to the always helpful people on IRC #tryton@freenode</p>Tryton Module Development2018-09-28T12:03:02+00:002024-03-26T10:13:24.359948+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/tryton-module-development/<p>So I've finally got around to really start Tryton module dev to customize it to what we need.</p>
<p>I plan to put stuff that is useful as examples or maybe directly as-is on my github: <a>https://github.com/LordVan/tryton-modules</a></p>
<p>On a side note this is trytond-4.8.4 running on python 3.5 at the moment.</p>
<p>The first module just (re-)adds the description filed to the sale lines in the sale module (entry). This by itself is vaguely useful for me but mostly was to figure out how this works. I have to say once figured out it is really easy - the hardest part was to get the XML right for someone who is not familiar with the structure. I'd like to thank the people who helped me on IRC ( #tryton@freenode )</p>
<p>The next step will be to add some custom fields to this and products.</p>
<p>To add this module you can follow the steps in the documentation: <a href="https://tryton-documentation.readthedocs.io/en/latest/developer_guide/example_library_1.html">Tryton by example</a></p>
<p></p>django DurationField - (very) simple way to format (for template)2018-07-24T06:09:10.091449+00:002024-03-28T18:56:07.697232+00:00lordvanhttps://blog.lordvan.com/blog/author/lordvan/https://blog.lordvan.com/blog/django-durationfield-very-simple-way-to-format-for-template/<p>So I have a Django <a href="https://docs.djangoproject.com/en/2.0/ref/models/fields/#durationfield">DurationField </a>in my model, and needed to format this as HH:mm .. unfortunately django doesn't seem to support that out of the box.. after considering templatetags or writing my own filter I decided to go for a very simple alternative and just defined a method for this in my model:</p>
<pre> timeslot_duration = models.DurationField(null=False,<br/> blank=False,<br/> default='00:05:00',<br/> verbose_name=_('timeslot_duration'),<br/> help_text=_('[DD] [HH:[MM:]]ss[.uuuuuu] format')<br/> )<br/><br/> def timeslot_duration_HHmm(self):<br/> sec = self.timeslot_duration.total_seconds()<br/> return '%02d:%02d' % (int((sec/3600)%3600), int((sec/60)%60))</pre>
<p>that way I can do whatever I want format-wise to get exactly what I need. Not sure if this is recommended practice, or maybe frowned upon, but it works just fine.</p>
<p>and in my template then just use <code>{{ <model>.timeslot_duration_HHmm }}</code> instead of <code>{{ <model>.timeslot_duration }}</code>.</p>