Das OpenERP System wurde ursprünglich unter dem Namen TinyERP entwickelt. Inzwischen wird von uns aktuell die Version 8.0 unter dem Namen odoo Bausteine in der cBUZZ OFFICE Suite - verwendet.
Das odoo Admin Buch für die cBUZZ OFFICE Suite kommt in der Creative Commons Lizenz CC-BY-NA-SA zum Leser und hilft über konkrete „Use Cases“ dem Systemadministrator bei der Abbildung des Kunden-Workflow in der cBUZZ OFFICE Suite. Im Folgenden werden beispielhaft Lösungen bei der Konfiguration und Administration der odoo Bausteine in der cBUZZ OFFICE Suite gezeigt.
Sie finden die Bücher von cBUZZ PRESS auch auf der Seite http://books.IFO.net
Als Admin müssen Sie natürlich die Domain test.IhreDomain.at im Nameserver auf die IP Adresse Ihres cBUZZ SERVERs zeigen lassen und dem Apache Server zB. im Virtualmin ihres cBUZZ SERVERs mitteilen, wie er auf http://test.IhreDomain.at reagieren soll! Natürlich können Sie diese Einstellungen auch bei der IFO.net Technik in Auftrag geben.
im Fenster Konfiguration Buchhaltung wählen Sie jetzt Österreichische Buchführung. Die Perioden werden Sie auf die Meldung Ihre Umsatzsteuer (monatlich oder vierteljährlich) abstimmen. Eine Umstellung der Perioden ist auch später immer wieder möglich.
Danach antworten Sie im Fenster Einstellung der Buchhaltung und wählen zB. einen vierstelligen Kontorahmen für Ihr Unternehmen !!!
Sie haben soeben als Adminstrator die stabile Grundlage für einen neuen Mandanten für Ihre odoo APPs in der cBUZZ OFFICE Suite gelegt.
Sie haben jetzt als Adminstrator eine neue Instanz für Ihre odoo APPs in der cBUZZ OFFICE Suite wie Website Builder, eComerce Online Shop, BLOG, Event Kalender oder Gemeinsame Notizen, Zusammenarbeit im Team, oder zur Rechnungserstellung, Buchhaltung, Lagerverwaltung, oder zur Verwaltung Ihrer Mitarbeiter, Lieferanten, Kunden und Offenen Posten erstellt. Die Installation dieser APPs funktioniert einfach über den Menüpunkt Einstellungen > Module > Odoo Apps.
Das cBUZZ OFFICE Dokument Management System verbindet virtuelle Ordner mit Dokumenten. Der virtuelle Ordner kann verwendet werden, um Anhänge eines Dokumentes zu verwalten, herunterzuladen und zu drucken. Die Verzeichnisse werden automatisch entsprechend der installierten Module angelegt.
Rufen Sie die Konfigurationshilfe einfach unter Einstellungen > Konfiguration > Dokumente auf. Es wird automatisch die App document installiert,
Dieser Wizard konfiguriert Ihre Verzeichnisse automatisch entsprechend der installierten Module. Sie erreichen damit:
Bezeichnung | Status |
---|---|
website | Installed |
sale | Installed |
payment | Installed |
Wenn Sie für eine B2B-Plattform das Shop-Menü und die E-Commerce-Inhalte (Produktseiten, etc.) für alle anonymen Website Besucher verstecken möchten, erreichen Sie das über zwei Einstellungen.
<t name="Products" t-name="website_sale.products">auf
<t name="Products" t-name="website_sale.products" groups="base.group_user,base.group_portal">
Jetzt können Sie Ihre Website als anonyme Person aufrufen. Sie werden das Shop-Menü nicht mehr sehen und wenn Sie die Seite http://IhreDomain.at/shop aufrufen, werden Sie eine leere Seite erhalten.
Siehe auch: https://github.com/odoo/odoo/issues/440
Die odoo Bausteine in der cBUZZ OFFICE Suite unterstützen einheitliche Berichte, indem sie auf eine allgemeinen Layout-Struktur aufbauen, die die einzelnen Zeilen in zwölf Spalten unterteilen.
Jedes Blatt hat einen oberen, einen unteren, einen linken und einen rechten Rand in Millimetern, die in Einstellungen ‣ Technisch ‣ Berichte ‣ Papierformat festgelegt werden.
Die odoo Module in der cBUZZ OFFICE Suite unterteilen ein Blatt in Reihen (CSS-Klasse row, also "Zeile") mit einem oberen Rand (CSS-Klasse mt, also "MarginTop") und einem unteren Rand (CSS-Klasse mb, also "MarginBottom"). Möchten Sie eine Zeile mit 8px oberen Rand und 32px unteren Rand einstellen, können Sie das mit folgendem Code definieren:
<div class="row mt8 mb32">Ihre neue Zeile!</div>
Mögliche Ränder sind: mt0, mt4, mt8, mt16, mt32, mt48, mt64, mt92, mt128, mb0, mb4, mb8, mb16, mb32, mb48, mb64, mb92, mb128
Jede Zeile wird in zwölf Spalten unterteilt. Möchten Sie eine bestimmte Position und Breite eines div Containers angeben? Ihnen stehen dafür zwei CSS-Klassennamen zu Verfügung:
Möchten Sie einen zentrierten div Container mit einer Breite von 50% anlegen? Verwenden Sie einfach folgenden Eintrag:
<div class="col-xs-6 col-xs-offset-3">Großer zentrierter Container</div>
col-xs-6 bedeutet eine Breite von 6 Spalten. Da es insgesamt 12 Spalten gibt, ist das genau die Hälfte davon, also 50%.
col-xs-offset-3 bedeutet, dass die Anzeige des Behälters nach einer Breite von 3 Spalten beginnt, es bleiben somit auch 3 Spalten bis zum rechten Rand. So ist der Behälter genau in der Mitte des Blattes.
Erhältliche Bildschirmgrößen
xs (Breite kleiner als 768px), sm (mindest-Breite: 768px), md (mindest-Breite: 992px), lg (mindest-Breite: 1200px)
Verfügbare Spaltenfaktoren
col-xs-offset- 1,2,3,4,5,6,7,8,9,10,11,12
Erhältliche Positionstypen
Beispiel: Möchten Sie einen kleinen Behälter (nur eine Spalten breit) auf der rechten Seite des Großen zentrierten Containers (siehe Beispiel oben) im Abstand von zwei Spalten positionieren? Verwenden Sie einfach folgenden Code:
<div class="col-xs-1 col-xs-push-2">Kleiner Container auf der rechten Seite</div>
Zu diesem Thema können Sie gerne unter https://cbuzz.ifo.net/forum/entwicklung-9/question/wie-verandere-ich-meine-rechnungen-in-der-odoo-qweb-report-engine-9 Fragen stellen.
Den Rechnungs-Nummernkreis legen wir fest unter Einstellungen ‣ Konfiguration ‣ Finanzen unter der Überschrift E-Rechnungen & Zahlungen im Feld Nächste Rechnungsnummer.
Default | Beispiel | Ergebnis |
VK/%(year)s/ | AR/%(y)s | AR/0001 |
GSV/%(year)s/ | GS-AR/%(y)s | GS-AR/0001 |
EK/%(year)s/ | ER/%(y)s | ER/0001 |
GSL/%(year)s/ | GS-ER/%(y)s | GS-ER/0001 |
Weiter Informationen dazu findet man im Buch odoo Bausteine in der cBUZZ OFFICE Suite unter Anpassung der Nummernkreise.
Die Benutzerdefinierte Fußzeile, das Firmenlogo und die (alte RML) Konfiguration finden wir unter:
Rufe Einstellungen ‣ Technisch‣ Berichte ‣ Berichte auf und suche nach Rechnungen und rufe den Eintrag auf. Wähle Suche nach verknüpften QWeb Ansichten. So findet man report_invoice und report_invoice_document für Anpassungen.
Damit die Kundenadresszeile für ein Fensterkuvert links ausgedruckt wird, können wir in report_invoice_document folgende Änderungen erstellen.
<div class="row">
<div class="col-xs-6 col-xs-offset-0"> <!-- div class="col-xs-5 col-xs-offset-7" --> <address t-field="o.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "address","zip"], "no_marker": true}' />
<!-- address t-field="o.partner_id" t-field-options="{"widget": "contact", "fields": ["address", "name"], "no_marker": true}"/ -->
<span t-field="o.partner_id.vat"/>
</div>
Um die Kopf- und Fußzeile für Angebot, Auftragsbestätigung, Rechnung, usw. in der neuen Qweb Technik bearbeiten zu können rufe folgendes auf:
Die Datei external_layout_header könnte dann so angepasst werden:
<?xml version="1.0"?> <t t-name="report.external_layout_header"> <div class="header"> <div class="row"> <!-- div class="col-xs-3" --> <div class="col-xs-12 col-xs-offset-0 text-right"> <img width="220" src="https://ifo.ifo.net/website/image/ir.attachment/270_17ecfd3/datas" alt="IFO.net" /><br/> <img width="600" height="8" src="https://ifo.ifo.net/website/image/ir.attachment/275_5b334fc/datas" alt="Linie" /> <br/>Regensburg - WIEN - Graz - KIRCHBACH .::. <strong>IFO.net Internet Service GmbH</strong><br/> <!-- img t-if="company.logo" t-att-src="'data:image/png;base64,%s' % company.logo" style="max-height: 45px;"/ --> </div> <!-- div class="col-xs-9 text-right" style="margin-top:20px;" t-field="company.rml_header1"/ --> </div> <!-- div class="row zero_min_height"> <div class="col-xs-12"> <div style="border-bottom: 1px solid black;"/> </div> </div --> <div class="row"> <!-- div class="col-xs-3" --> <div class="col-xs-6 col-xs-offset-6 text-right"> Nr 79 - imPULSzentrum.eu<br/> 8082 Kirchbach in Steiermark / AUSTRIA<br/><br/> Tel: +43.3116/2021-0 Fax: 2021-22<br/> eMail: service@IFO.net <strong>http://IFO.net</strong> <!-- div t-field="company.partner_id" t-field-options="{"widget": "contact", "fields": ["address", "name"], "no_marker": true}" style="border-bottom: 1px solid black;"/ --> </div> </div> </div> </t>
Die Datei external_layout_footer könnte danach so angepasst werden:
<?xml version="1.0"?> <t t-name="report.external_layout_footer"> <div class="footer"> <div class="col-xs-12 col-xs-offset-0 text-right"> cBUZZ steht für CORPORATE BUSINESS für Firmenkunden .::. www.cBUZZ.eu <img width="762" height="8" src="https://ifo.ifo.net/website/image/ir.attachment/275_5b334fc/datas" alt="Linie" /> <span style="font-size:11px"> <br/>Es gelten die allgemeinen Geschäftsbedingungen der IFO.net Internet Service GmbH, die Sie unter http://agb.IFO.net finden. Jeder Garantieanspruch<br/> beschränkt sich auf die Behebung des Fehlers. Für die Datensicherung ist der Kunde selbst verantwortlich. Für Folgeschäden kann keine Haftung<br/> übernommen werden. Gerichtsstand Graz. Bankkonto Ihre Bank IBAN: AT99 2222 0000 0012 2550 BIC: CAWWAT2G170 </span> <span style="font-size:10px"> <div class="text-center" style="border-top: 0px solid black;"> <ul class="list-inline"> <li/> <li> Seite: <span class="page"/> </li> <li> von: <span class="topage"/> <ul class="list-inline js_language_selector mt16"> </ul> </li> </ul> </div> </span> <!-- div class="text-center" style="border-top: 1px solid black;"> <ul t-if="not company.custom_footer" class="list-inline"> ............ </ul> </div --> </div> </div> </t>
Rufe Einstellungen ‣ Technisch‣ Berichte ‣ Berichte auf und suche nach Mahnung und rufe den Eintrag Statistik Mahnungen (account_followup.report_followup) auf. Klicke auf Suche nach verknüpften QWeb Ansichten. So findet man report_followup für Anpassungen.
Damit die Kundenadresszeile für ein Fensterkuvert links ausgedruckt wird, können wir in report_followup folgende Änderungen erstellen.
<div class="page"> <div class="row"> <div class="col-xs-5 col-xs-offset-0"> <span style="font-size:11px"><br/><br/><br/>Absender: IFO.net GmbH, 8082 Kirchbach Nr. 79 / AUSTRIA<br/></span> <div class="text-center" style="border-top: 1px solid black;" /> <br/>     <span t-field="o.partner_id.name"/> <span t-if="o.partner_id.street2"><br/>     <span t-field="o.partner_id.street2"/></span> <br/>     <span t-field="o.partner_id.street"/> <br/>     <span t-field="o.partner_id.zip"/> <span t-field="o.partner_id.city"/> <br/>     <span t-field="o.partner_id.country_id"/><br/><br/>
<!-- address t-field="o.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "address", "zip"], "no_marker": true}' /--> <!-- address t-field="o.partner_id" t-field-options="{"widget": "contact", "fields": ["address", "name"], "no_marker": true}"/ -->
</div> </div>
Rufe Finanzen ‣ Konfiguration ‣ Verschiedenes ‣ E-Mail-vorlagen auf und suche nach follow-up oder Mahnung.
Betreff ${user.company_id.name} Zahlungserinnerung
Dear ${object.name},
We are disappointed to see that despite sending a reminder, that your account is now seriously overdue. It is essential that immediate payment is made, otherwise we will have to consider placing a stop on your account which means that we will no longer be able to supply your company with (goods/services). Please, take appropriate measures in order to carry out this payment in the next 8 days. If there is a problem with paying invoice that we are not aware of, do not hesitate to contact our accounting department. so that we can resolve the matter quickly. Details of due payments is printed below.
Best Regards,
${user.name}
${object.get_followup_table_html() | safe}
Betreff Mahnung für ${object.name} von ${user.company_id.name}
Sehr geehrte Kundin, sehr geehrter Kunde,
wir sind enttäuscht, dass Ihr Konto trotz Mahnung überfällig ist.
Wichtig ist jetzt, dass Sie für eine sofortige Zahlung sorgen, damit eine kontinuierliche Nutzung unserer Dienstleistungen für Sie gewährleistet bleibt.
Bitte zahlen Sie OFFENE Beträge umgehend auf unser Raiffeisen Konto IBAN: AT19 3817 0000 0000 2550 | BIC: RZSTAT2G170 ein, oder füllen Sie auf der Seite http://konto.IFO.net das Formular aus, für eine Bezahlung im SEPA Lastschriftverfahren über Ihr IBAN Konto.
Ihr Kontakt: business@IFO.net oder +43.664.4559677. Wenn Sie Fragen haben, zögern Sie bitte nicht uns zu kontaktieren.
Besten Dank für Ihr Interesse an unseren Produkten und Dienstleistungen.
IFO.net Internet Service GmbH
${object.get_followup_table_html() | safe}
Betreff ${user.company_id.name} Zahlungserinnerung
Dear ${object.name},
Exception made if there was a mistake of ours, it seems that the following amount stays unpaid. Please, take appropriate measures in order to carry out this payment in the next 8 days. Would your payment have been carried out after this mail was sent, please ignore this message. Do not hesitate to contact our accounting department.
Best Regards,
${user.name}
${object.get_followup_table_html() | safe}
Betreff Mahnung für ${object.name} von ${user.company_id.name}
Sehr geehrte Kundin, sehr geehrter Kunde,
bitte kontrollieren Sie folgenden Kontoauszug und überweisen Sie umgehend OFFENE Beträge auf unser Raiffeisen Konto IBAN: AT19 3817 0000 0000 2550 | BIC: RZSTAT2G170.
TIPP: Sichern Sie sich die kontinuierliche Nutzung unserer Dienstleistungen und füllen Sie einmalig das Lastschriftformular unter http://konto.IFO.net aus. Das ist bequem und sicher! Sie brauchen die Rechnungen der IFO.net GmbH dann nicht mehr einzahlen. Der Betrag wird jeweils im SEPA Lastschriftverfahren Ihrem IBAN Konto zur Bezahlung vorgelegt.
Ihr Kontakt: business@IFO.net oder +43.664.4559677. Wenn Sie Fragen haben, zögern Sie bitte nicht uns zu kontaktieren.
Besten Dank für Ihr Interesse an unseren Produkten und Dienstleistungen.
IFO.net Internet Service GmbH
${object.get_followup_table_html() | safe}
Betreff ${user.company_id.name} Zahlungserinnerung
Dear ${object.name},
Despite several reminders, your account is still not settled. Unless full payment is made in next 8 days, legal action for the recovery of the debt will be taken without further notice. I trust that this action will prove unnecessary and details of due payments is printed below. In case of any queries concerning this matter, do not hesitate to contact our accounting department.
Best Regards,
${user.name}
${object.get_followup_table_html() | safe}
Betreff Mahnung für ${object.name} von ${user.company_id.name}
Sehr geehrte Kundin, sehr geehrter Kunde,
trotz mehrerer Mahnungen ist Ihr Konto noch immer nicht beglichen!
Nur durch eine sofortige Zahlung können Sie eine Sperre unserer Dienstleistungen und eine Klage der OFFENEN Beträge noch verhindern, die sonst ohne weitere Warnung erfolgen wird.
Bitte zahlen Sie die OFFENE Beträge sofort auf unser Raiffeisen Konto IBAN: AT19 3817 0000 0000 2550 | BIC: RZSTAT2G170 ein, oder füllen Sie auf der Seite http://konto.IFO.net das Formular aus, für eine Bezahlung im SEPA Lastschriftverfahren über Ihr IBAN Konto.
Bitte ersparen Sie sich die erheblichen Mehrkosten, die durch die weiteren technischen und rechtlichen Schritte bereits in den nächsten Tagen entstehen werden.
Ihr Kontakt: business@IFO.net oder +43.664.4559677. Wenn Sie Fragen haben, zögern Sie bitte nicht uns zu kontaktieren.
Besten Dank für Ihr Interesse an unseren Produkten und Dienstleistungen.
IFO.net Internet Service GmbH
${object.get_followup_table_html() | safe}
Rufe Einstellungen ‣ Technisch‣ Berichte ‣ Berichte auf und suche nach Angebot oder order und rufe das Modell sale.order (Angebot / Bestellung) auf. Klicke auf Suche nach verknüpften QWeb Ansichten. So findet man
report_sale_order_document für Anpassungen.
Damit die Kundenadresszeile für ein Fensterkuvert links ausgedruckt wird, können wir in report_sale_order_document das Original:
<div class="row">
<div class="col-xs-6">
<strong t-if="o.partner_shipping_id == o.partner_invoice_id">Invoice and shipping address:</strong>
<strong t-if="o.partner_shipping_id != o.partner_invoice_id">Invoice address:</strong>
<div t-field="o.partner_invoice_id" t-field-options="{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}"/>
<p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
<div t-if="o.partner_shipping_id != o.partner_invoice_id" class="mt8">
<strong>Shipping address:</strong>
<div t-field="o.partner_shipping_id" t-field-options="{"widget": "contact", "fields": ["address", "name", "phone", "fax"], "no_marker": true}"/>
<p t-if="o.partner_id.vat">VAT: <span t-field="o.partner_id.vat"/></p>
</div>
</div>
<div class="col-xs-5 col-xs-offset-1">
<div t-field="o.partner_id" t-field-options="{"widget": "contact", "fields": ["address", "name"], "no_marker": true}"/>
</div>
</div>
ändern in:
<div class="row"> <div class="col-xs-5 col-xs-offset-0"> <span style="font-size:11px"><br/><br/><br/>Absender: IFO.net GmbH, 8082 Kirchbach Nr. 79 / AUSTRIA<br/></span> <div class="text-center" style="border-top: 1px solid black;" /> <br/>     <span t-field="o.partner_id.name"/> <span t-if="o.partner_id.street2"><br/>     <span t-field="o.partner_id.street2"/></span> <br/>     <span t-field="o.partner_id.street"/> <br/>     <span t-field="o.partner_id.zip"/> <span t-field="o.partner_id.city"/> <br/>     <span t-field="o.partner_id.country_id"/><br/><br/>
<!-- address t-field="o.partner_id" t-field-options='{"widget": "contact", "fields": ["name", "address", "zip"], "no_marker": true}' /--> <!-- address t-field="o.partner_id" t-field-options="{"widget": "contact", "fields": ["address", "name"], "no_marker": true}"/ -->
</div> </div>
Im obigen Beispiel verzichten wir auf die Spalte Lieferadresse, weil wir ohnehin nur Dienstleistungen verkaufen!
Rufe Finanzen ‣ Konfiguration ‣ Verschiedenes ‣ E-Mail-vorlagen auf und suche nach sale oder Angebot.
Rufe Finanzen ‣ Verschiedenes ‣ E-Mail-Vorlagen auf und suche nach Invoice oder Rechnung.
Rufe die Vorlage auf und klicke links oben auf Bearbeiten. In der Zeile mit den Icons kann man mit Show Source auch direkt den HTML Quelltext bearbeiten.
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: #FFF; ">
<p>An: ${object.partner_id.name}</p><p>Sehr geehrter Kunde, <br></p><p>besten Dank für Ihr Interesse an unseren Produkten und Dienstleistungen.</p><p>Für Sie wurde eine neue Rechnung erstellt, die Sie im Anhang erhalten:<br></p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
<strong>REFERENZEN</strong><span id="result_box" class="short_text" lang="de"><span class="hps alt-edited"></span></span><br>
Rechnungs Nummer: <strong>${object.number}</strong><br>
Rechnungs Betrag: <strong>${object.amount_total} ${object.currency_id.name}</strong><br>
Rechnungs Datum: ${object.date_invoice}<br>
% if object.origin:
Order reference: ${object.origin}<br>
% endif
% if object.user_id:
Ihr Kontakt: <span style="font-weight: bold;"><a href="mailto:business@IFO.net">business@IFO.net</a></span> oder +43.664.4559677
% endif
</p>
<p><br></p><p>Wenn Sie Fragen haben, zögern Sie bitte nicht uns zu kontaktieren.</p>
<p>Danke für Ihre Wahl.</p><p> ${object.company_id.name or 'us'}</p>
<br>
<br>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' %
object.company_id.state_id.name) or ''}
${object.company_id.country_id.name or ''}<br>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone: ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web: <span style="font-weight: bold;"><a href="https://cBUZZ.IFO.net">https://cBUZZ.IFO.net</a></span>
</div>
%endif
</div>
</div>
Mit Hilfe dieses Beitrags kann ein Programmierer den Quellcode zum oddo System rasch lesen und für eigene Projekte anpassen.
Der Aufbau des odoo Systems läst sich als MVC (Model View Controller) Prinzip verstehen. Wobei die Darstellung der Daten vom Zugriff und der Speicherung getrennt ist.
Die Darstellung erfolgt im odoo Client (Web Browser, Firefox, Google Chrome) und wird mit Hilfe von serverseitig abgelegten XML-Dateien festgelegt. Die Modelle liegen in Tabellen der PostgreSQL Datenbank. Die Schnittstelle zwischen dem Modell und der Ansicht ist ein Python-Controller-Objekt.
Die odoo APPs in der cBUZZ OFFICE Suite definieren in der Datei /etc/odoo/openerp-server.conf den addons_path
Unter /usr/lib/python2.7/dist-packages/openerp/addons erstellen wir das Verzeichnis: my-academy mit folgender Struktur:
├── my-academy
│ ├── __init__.py # Der Einsprungpunkt für ihr Modul, hier binden sie alle weiteren Python Module ein
│ ├── __openerp__.py # Enthält verschieden Paket Informationen
│ ├── security/ #
│ │ ├── ir.model.access.csv
│ ├── controllers.py # Das Modul mit ihren Objektdefinitionen
│ ├── static/ # Das static Verzeichnis wird angelegt, bleibt aber noch leer│ ├── views/ # Definieren Sie hier ihre Ansichten (Aktionen, Wizards, Menüs, Masken)
│ │ ├── demo.xml
│ │ ├── templates.xml
mkdir /usr/lib/python2.7/dist-packages/openerp/addons/my-academy
chmod -R 755 /usr/lib/python2.7/dist-packages/openerp/addons/my-academy
./my-academy/__init__.py # Der Einsprungpunkt für ihr Modul, hier binden sie alle weiteren Python Module ein:
import controllers # Einbinden des Moduls, das Hallo Welt :-) ausgeben wird.
./my-academy/__openerp__.py # oder __oddo__.py steuert die APP Installation inkl. Abhängigkeiten mit Paketbeschreibung.
{
'name': 'My Academy',
'version': '1.0',
'category': 'Tools',
'summary': 'Courses, Sessions, Subscriptions',
'description': """
Courses, Sessions, Subscriptions
================================
This module manages the definition of course, the organization,
sessions, and the subscriptions of participants.
""",
'depends' : ['web'],
'data' : [
'views/demo.xml',
'views/templates.xml',
],
'images': [],
'demo': [],
'application': True,
'installable' : True,
}
./my-academy/security/ir.model.access.csv # Steuert Zugangsrechte (brauchen wir erst später im Beispiel)
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_academy_academy,academy.academy,model_academy_academy,,1,0,0,0
./my-academy/controllers.py # Das Modul mit ihrer Objektdefinition wird Hallo Welt :-) ausgeben
# -*- coding: utf-8 -*-
from openerp import http
class Academy(http.Controller):
@http.route('/academy/academy/', auth='public')
def index(self, **kw):
return "Hallo Welt :-)"
# @http.route('/academy/academy/objects/', auth='public')
# def list(self, **kw):
./my-academy/static/
./my-academy/views/demo.xml # Definiert die Datenfelder (brauchen wir erst später im Beispiel)
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- -->
<!-- <record id="object0" model="academy.academy"> -->
<!-- <field name="name">Object 0</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object1" model="academy.academy"> -->
<!-- <field name="name">Object 1</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object2" model="academy.academy"> -->
<!-- <field name="name">Object 2</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object3" model="academy.academy"> -->
<!-- <field name="name">Object 3</field> -->
<!-- </record> -->
<!-- -->
<!-- <record id="object4" model="academy.academy"> -->
<!-- <field name="name">Object 4</field> -->
<!-- </record> -->
<!-- -->
</data>
</openerp>
./my-academy/views/templates.xml # Definiert das QWeb Formular (brauchen wir erst später im Beispiel)
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- <template id="listing"> -->
<!-- <ul> -->
<!-- <li t-foreach="objects" t-as="object"> -->
<!-- <a t-attf-href="#{ root }/objects/#{ object.id }"> -->
<!-- <t t-esc="object.display_name"/> -->
<!-- </a> -->
<!-- </li> -->
<!-- </ul> -->
<!-- </template> -->
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->
<!-- <t t-foreach="object._fields" t-as="field"> -->
<!-- <dt><t t-esc="field"/></dt> -->
<!-- <dd><t t-esc="object[field]"/></dd> -->
<!-- </t> -->
<!-- </dl> -->
<!-- </template> -->
</data>
</openerp>
Wenn Sie alle Dateien angelegt haben, starten Sie odoo neu:
/etc/init.d/odoo restart
Im odoo Back-End kann der Admin die lokale My Acadamy APP jetzt installieren.
Einstellungen ‣ Benutzer ‣ Benutzer
Als Entwickler können Sie auch den Entwicklermodus aktivieren. Klicken Sie rechts oben Ihr Benutzerkonto an und wählen Sie das Feld Über odoo. Im neuen Fenster klicken Sie auf Entwicklermodus wählen.
Der einfache Controllers interpretiert den Browser Aufruf und sendet Daten zurück:
Das generieren von HTML ist in purem Python war niemals sehr angenehm.
Die übliche Lösung sind ist Templates. Also Pseudo-Dokumente mit Platzhaltern und gedachter Anzeigelogik. Mit odoo können Sie jedes Python Template-System verwenden. Seit der Version odoo V8 steht Ihnen aber auch das odoo eigene QWeb Template-System zur Verfügung, das mit anderen odoo Funktionen interagiert.
Ergänzen Sie jetzt die beiden vorhin erstellten Dateien mit den im Quellcode fett gekennzeichneten Stellen:
./my-academy/controllers.py # Das Modul mit ihrer Objektdefinition wird Hallo Welt :-) ausgeben
class Academy(http.Controller): @http.route('/academy/academy/', auth='public') def index(self, **kw): return http.request.render('my-academy.index', { 'teachers': ["Diana Padilla", "Jody Caroll", "Lester Vaughn"], }) # @http.route('/academy/academy/objects/', auth='public') # def list(self, **kw):
./my-academy/views/templates.xml # Definiert das QWeb Formular
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<template id="index">
<title>Academy</title>
<t t-foreach="teachers" t-as="teacher">
<p><t t-esc="teacher"/></p>
</t>
</template>
<!-- <template id="object"> -->
<!-- <h1><t t-esc="object.display_name"/></h1> -->
<!-- <dl> -->
The templates iterates (t-foreach) on all the teachers (passed through the template context), and prints each teacher in its own paragraph.
Finally restart Odoo and update the module’s data (to install the template) by going to Settings ‣ Modules ‣ Local Modules ‣ Academy and clicking Upgrade.
Alternativ können Sie odoo auch auf der Command-Line neu starten:
/etc/init.d/odoo restart
Odoo models map to database tables.
In the previous section we just displayed a list of string entered statically in the Python code. This doesn’t allow modifications and persistent storage thereof, so we’re now going to move our data to the database.
Define a teacher model, and ensure it is imported from __init__.py so it is correctly loaded:
academy/models.py
from openerp import models, fields, api class Teachers(models.Model): _name = 'academy.teachers' name = fields.Char()
Then setup basic access control for the model and add them to the manifest:
academy/__openerp__.py
# always loaded 'data': [ 'security/ir.model.access.csv', 'templates.xml', ], # only loaded in demonstration mode
academy/security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
this simply gives read access (perm_read) to all users (group_id:id left empty).
Note
Data files (XML or CSV) have to be added to the module manifest, Python files (models or controllers) don’t but have to be imported from __init__.py (directly or indirectly)
Warning
the administrator user bypasses access control, he has access to all models even if not given access
The second step is to add some demonstration data to the system so it’s possible to test it easily. This is done by adding a demo data file, which must be linked from the manifest:
academy/demo.xml
<openerp> <data> <record id="padilla" model="academy.teachers"> <field name="name">Diana Padilla</field> </record> <record id="carroll" model="academy.teachers"> <field name="name">Jody Carroll</field> </record> <record id="vaughn" model="academy.teachers"> <field name="name">Lester Vaughn</field> </record> </data> </openerp>
Data files can be used for demo and non-demo data. Demo data are only loaded in “demonstration mode” and can be used for flow testing and demonstration, non-demo data are always loaded and used as initial system setup.
In this case we’re using demonstration data because an actual user of the
system would want to input or import their own teachers list, this list
is only useful for testing.
The last step is to alter model and template to use our demonstration data:
academy/controllers.py
class Academy(http.Controller): @http.route('/academy/academy/', auth='public') def index(self, **kw): Teachers = http.request.env['academy.teachers'] return http.request.render('academy.index', { 'teachers': Teachers.search([]) }) # @http.route('/academy/academy/objects/', auth='public')
academy/templates.xml
<template id="index"> <title>Academy</title> <t t-foreach="teachers" t-as="teacher"> <p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p> </t> </template> <!-- <template id="object"> -->
Restart the server and update the module (in order to update the manifest and templates and load the demo file) then navigate to http://localhost:8069/academy/academy/. The page should look little different: names should simply be prefixed by a number (the database identifier for the teacher).
Odoo bundles a module dedicated to building websites.
So far we’ve used controllers fairly directly, but Odoo 8 added deeper integration and a few other services (e.g. default styling, theming) via the website module.
academy/__openerp__.py
'version': '0.1', # any module necessary for this one to work correctly 'depends': ['website'], # always loaded 'data': [
academy/controllers.py
from openerp import http class Academy(http.Controller): @http.route('/academy/academy/', auth='public', website=True) def index(self, **kw): Teachers = http.request.env['academy.teachers'] return http.request.render('academy.index', {
academy/templates.xml
<openerp> <data> <template id="index"> <t t-call="website.layout"> <t t-set="title">Academy</t> <div class="oe_structure"> <div class="container"> <t t-foreach="teachers" t-as="teacher"> <p><t t-esc="teacher.id"/> <t t-esc="teacher.name"/></p> </t> </div> </div> </t> </template> <!-- <template id="object"> -->
After restarting the server while updating the module (in order to update the manifest and template) access http://localhost:8069/academy/academy/ should yield a nicer looking page with branding and a number of built-in page elements (top-level menu, footer, …)
The website layout also provides support for edition tools: click Sign In (in the top-right), fill the credentials in (admin / admin by default) then click Log In.
You’re now in Odoo “proper”: the administrative interface. For now click on the Website menu item (top-left corner.
We’re back in the website but as an administrator, with access to advanced edition features provided by the website support:
Controller methods are associated with routes via the route() decorator which takes a routing string and a number of attributes to customise its behavior or security.
We’ve seen a “literal” routing string, which matches a URL section exactly, but routing strings can also use converter patterns which match bits of URLs and make those available as local variables. For instance we can create a new controller method which takes a bit of URL and prints it out:
academy/controllers.py
'teachers': Teachers.search([]) }) @http.route('/academy/<name>/', auth='public', website=True) def teacher(self, name): return '<h1>{}</h1>'.format(name) # @http.route('/academy/academy/objects/', auth='public') # def list(self, **kw): # return http.request.render('academy.listing', {
restart Odoo, access http://localhost:8069/academy/Alice/ and http://localhost:8069/academy/Bob/ and see the difference.
As the name indicates, converter patterns don’t just do extraction, they also do validation and conversion, so we can change the new controller to only accept integers:
academy/controllers.py
'teachers': Teachers.search([]) }) @http.route('/academy/<int:id>/', auth='public', website=True) def teacher(self, id): return '<h1>{} ({})</h1>'.format(id, type(id).__name__) # @http.route('/academy/academy/objects/', auth='public')
Restart Odoo, access http://localhost:8069/academy/2, note how the old value was a string, but the new one was converted to an integers. Try accessing http://localhost:8069/academy/Carol/ and note that the page was not found: since “Carol” is not an integer, the route was ignored and no route could be found.
Odoo provides an additional converter called model which provides records directly when given their id, let’s use that and create a generic page for teacher biographies:
academy/controllers.py
'teachers': Teachers.search([]) }) @http.route('/academy/<model("academy.teachers"):teacher>/', auth='public', website=True) def teacher(self, teacher): return http.request.render('academy.biography', { 'person': teacher }) # @http.route('/academy/academy/objects/', auth='public')
academy/templates.xml
</div> </t> </template> <template id="biography"> <t t-call="website.layout"> <t t-set="title">Academy</t> <div class="oe_structure"/> <div class="oe_structure"> <div class="container"> <p><t t-esc="person.id"/> <t t-esc="person.name"/></p> </div> </div> <div class="oe_structure"/> </t> </template> <!-- <template id="object"> --> <!-- <h1><t t-esc="object.display_name"/></h1> --> <!-- <dl> -->
then change the list of model to link to our new controller:
academy/templates.xml
<div class="oe_structure"> <div class="container"> <t t-foreach="teachers" t-as="teacher"> <p><a t-attf-href="/academy/{{ slug(teacher) }}"> <t t-esc="teacher.name"/></a> </p> </t> </div> </div>
<div class="oe_structure"/> <div class="oe_structure"> <div class="container"> <h3><t t-esc="person.name"/></h3> </div> </div> <div class="oe_structure"/>
Restart Odoo and upgrade the module, then you can visit each teacher’s page. As an exercise, try adding blocks to a teacher’s page to write a biography, then go to another teacher’s page and so forth. You will discover, that your biography is shared between all teachers, because blocks are added to the template, and the biography template is shared between all teachers, when one page is edited they’re all edited at the same time.
Data which is specific to a record should be saved on that record, so let us add a new biography field to our teachers:
academy/models.py
_name = 'academy.teachers' name = fields.Char() biography = fields.Html()
academy/templates.xml
<div class="oe_structure"> <div class="container"> <h3><t t-esc="person.name"/></h3> <div><t t-esc="person.biography"/></div> </div> </div> <div class="oe_structure"/>
Restart Odoo and update the views, reload the teacher’s page and… the field is invisible since it contains nothing.
For record fields, templates can use a special t-field directive which allows editing the field content from the website using field-specific interfaces. Change the person template to use t-field:
academy/templates.xml
<div class="oe_structure"/> <div class="oe_structure"> <div class="container"> <h3 t-field="person.name"/> <div t-field="person.biography"/> </div> </div> <div class="oe_structure"/>
Restart Odoo and upgrade the module, there is now a placeholder under the teacher’s name and a new zone for blocks in Edit mode. Content dropped there is stored in the corresponding teacher’s biography field, and thus specific to that teacher.
The teacher’s name is also editable, and when saved the change is visible on the index page.
t-field can also take formatting options which depend on the exact field. For instance if we display the modification date for a teacher’s record:
academy/templates.xml
<div class="oe_structure"> <div class="container"> <h3 t-field="person.name"/> <p>Last modified: <i t-field="person.write_date"/></p> <div t-field="person.biography"/> </div> </div>
it is displayed in a very “computery” manner and hard to read, but we could ask for a human-readable version:
academy/templates.xml
<div class="oe_structure"> <div class="container"> <h3 t-field="person.name"/> <p>Last modified: <i t-field="person.write_date" t-field-options='{"format": "long"}'/></p> <div t-field="person.biography"/> </div> </div>
or a relative display:
academy/templates.xml
<div class="oe_structure"> <div class="container"> <h3 t-field="person.name"/> <p>Last modified: <i t-field="person.write_date" t-field-options='{"widget": "relative"}'/></p> <div t-field="person.biography"/> </div> </div>
The Odoo administration was briefly seen during the website support section. We can go back to it using Administrator ‣ Administrator in the menu (or Sign In if you’re signed out).
The conceptual structure of the Odoo backend is simple:
By default, an Odoo model is essentially invisible to a user. To make it visible it must be available through an action, which itself needs to be reachable, generally through a menu.
Let’s create a menu for our model:
academy/__openerp__.py
'data': [ 'security/ir.model.access.csv', 'templates.xml', 'views.xml', ], # only loaded in demonstration mode 'demo': [
academy/views.xml
<openerp><data> <record id="action_academy_teachers" model="ir.actions.act_window"> <field name="name">Academy teachers</field> <field name="res_model">academy.teachers</field> </record> <menuitem sequence="0" id="menu_academy" name="Academy"/> <menuitem id="menu_academy_content" parent="menu_academy" name="Academy Content"/> <menuitem id="menu_academy_content_teachers" parent="menu_academy_content" action="action_academy_teachers"/> </data></openerp>
then accessing http://localhost:8069/web/ in the top left should be a menu Academy, which is selected by default, as it is the first menu, and having opened a listing of teachers. From the listing it is possible to Create new teacher records, and to switch to the “form” by-record view.
If there is no definition of how to present records (a view) Odoo will automatically create a basic one on-the-fly. In our case it works for the “list” view for now (only displays the teacher’s name) but in the “form” view the HTML biography field is displayed side-by-side with the name field and not given enough space. Let’s define a custom form view to make viewing and editing teacher records a better experience:
academy/views.xml
<field name="res_model">academy.teachers</field> </record> <record id="academy_teacher_form" model="ir.ui.view"> <field name="name">Academy teachers: form</field> <field name="model">academy.teachers</field> <field name="arch" type="xml"> <form> <sheet> <label for="name"/> <field name="name"/> <label for="biography"/> <field name="biography"/> </sheet> </form> </field> </record> <menuitem sequence="0" id="menu_academy" name="Academy"/> <menuitem id="menu_academy_content" parent="menu_academy" name="Academy Content"/>
We have seen a pair of “basic” fields stored directly in the record. There are a number of basic fields. The second broad categories of fields are relational and used to link records to one another (within a model or across models).
For demonstration, let’s create a courses model. Each course should have a teacher field, linking to a single teacher record, but each teacher can teach many courses:
academy/models.py
name = fields.Char() biography = fields.Html() class Courses(models.Model): _name = 'academy.courses' name = fields.Char() teacher_id = fields.Many2one('academy.teachers', string="Teacher")
academy/security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0 access_academy_courses,access_academy_courses,model_academy_courses,,1,0,0,0
let’s also add views so we can see and edit a course’s teacher:
academy/views.xml
</field> </record> <record id="action_academy_courses" model="ir.actions.act_window"> <field name="name">Academy courses</field> <field name="res_model">academy.courses</field> </record> <record id="academy_course_search" model="ir.ui.view"> <field name="name">Academy courses: search</field> <field name="model">academy.courses</field> <field name="arch" type="xml"> <search> <field name="name"/> <field name="teacher_id"/> </search> </field> </record> <record id="academy_course_list" model="ir.ui.view"> <field name="name">Academy courses: list</field> <field name="model">academy.courses</field> <field name="arch" type="xml"> <tree string="Courses"> <field name="name"/> <field name="teacher_id"/> </tree> </field> </record> <record id="academy_course_form" model="ir.ui.view"> <field name="name">Academy courses: form</field> <field name="model">academy.courses</field> <field name="arch" type="xml"> <form> <sheet> <label for="name"/> <field name="name"/> <label for="teacher_id"/> <field name="teacher_id"/> </sheet> </form> </field> </record> <menuitem sequence="0" id="menu_academy" name="Academy"/> <menuitem id="menu_academy_content" parent="menu_academy" name="Academy Content"/> <menuitem id="menu_academy_content_courses" parent="menu_academy_content" action="action_academy_courses"/> <menuitem id="menu_academy_content_teachers" parent="menu_academy_content" action="action_academy_teachers"/>
It should also be possible to create new courses directly from a teacher’s page, or to see all the courses a teacher gives, so add the inverse relationship to the teachers model:
academy/models.py
name = fields.Char() biography = fields.Html() course_ids = fields.One2many('academy.courses', 'teacher_id', string="Courses") class Courses(models.Model): _name = 'academy.courses'
academy/views.xml
<form> <sheet> <label for="name"/> <field name="name"/> <label for="biography"/> <field name="biography"/> <field name="course_ids"> <tree string="Courses" editable="bottom"> <field name="name"/> </tree> </field> </sheet> </form> </field>
Odoo provides technical models, which don’t fulfill business needs in and of themselves but add capabilities to business objects without having to build them by hand.
One of these is the Chatter system, part of Odoo’s email and messaging system, which can add notifications and discussion threads to any model. The model simply has to _inherit mail.thread, and add the message_ids field to its form view to display the discussion thread. Discussion threads are per-record.
For our academy, it makes sense to allow discussing courses to handle e.g. scheduling changes or discussions between teachers and assistants:
academy/models.py
class Courses(models.Model): _name = 'academy.courses' _inherit = 'mail.thread' name = fields.Char() teacher_id = fields.Many2one('academy.teachers', string="Teacher")
academy/views.xml
<label for="teacher_id"/> <field name="teacher_id"/> </sheet> <div class="oe_chatter"> <field name="message_follower_ids" widget="mail_followers"/> <field name="message_ids" widget="mail_thread"/> </div> </form> </field> </record>
At the bottom of each course form, there is now a discussion thread and the
possibility for users of the system to leave messages and follow or unfollow
discussions linked to specific courses.
Odoo also provides business models which allow using or opting in business needs more directly. For instance the website_sale module sets up an e-commerce site based on the products in the Odoo system. We can easily make course subscriptions sellable by making our courses specific kinds of products.
Rather than the previous classical inheritance, this means replacing our course model by the product model, and extending products in-place (to add anything we need to it).
First of all we need to add a dependency on website_sale so we get both products (via sale) and the ecommerce interface:
academy/__openerp__.py
'version': '0.1', # any module necessary for this one to work correctly 'depends': ['website_sale'], # always loaded 'data': [
restart Odoo, update your module, there is now a Shop section in the website, listing a number of pre-filled (via demonstration data) products.
The second step is to replace the courses model by product.template, and add a new category of product for courses:
academy/__openerp__.py
'security/ir.model.access.csv', 'templates.xml', 'views.xml', 'data.xml', ], # only loaded in demonstration mode 'demo': [
academy/data.xml
<openerp><data> <record model="product.public.category" id="category_courses"> <field name="name">Courses</field> <field name="parent_id" ref="website_sale.categ_others"/> </record> </data></openerp>
academy/demo.xml
<record id="vaughn" model="academy.teachers"> <field name="name">Lester Vaughn</field> </record> <record id="course0" model="product.template"> <field name="name">Course 0</field> <field name="teacher_id" ref="padilla"/> <field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/> <field name="website_published">True</field> <field name="list_price" type="float">0</field> <field name="type">service</field> </record> <record id="course1" model="product.template"> <field name="name">Course 1</field> <field name="teacher_id" ref="padilla"/> <field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/> <field name="website_published">True</field> <field name="list_price" type="float">0</field> <field name="type">service</field> </record> <record id="course2" model="product.template"> <field name="name">Course 2</field> <field name="teacher_id" ref="vaughn"/> <field name="public_categ_ids" eval="[(4, ref('academy.category_courses'), False)]"/> <field name="website_published">True</field> <field name="list_price" type="float">0</field> <field name="type">service</field> </record> </data> </openerp>
academy/models.py
name = fields.Char() biography = fields.Html() course_ids = fields.One2many('product.template', 'teacher_id', string="Courses") class Courses(models.Model): _inherit = 'product.template' teacher_id = fields.Many2one('academy.teachers', string="Teacher")
academy/security/ir.model.access.csv
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_academy_teachers,access_academy_teachers,model_academy_teachers,,1,0,0,0
academy/views.xml
</field> </record> <menuitem sequence="0" id="menu_academy" name="Academy"/> <menuitem id="menu_academy_content" parent="menu_academy" name="Academy Content"/> <menuitem id="menu_academy_content_teachers" parent="menu_academy_content" action="action_academy_teachers"/>
With this installed, a few courses are now available in the Shop, though they may have to be looked for.
Note
So far, we have briefly seen:
We’re left with the alteration of existing records and the alteration of existing views. We’ll do both on the Shop pages.
View alteration is done by creating extension views, which are applied on top of the original view and alter it. These alteration views can be added or removed without modifying the original, making it easier to try things out and roll changes back.
Since our courses are free, there is no reason to display their price on the shop page, so we’re going to alter the view and hide the price if it’s 0. The first task is finding out which view displays the price, this can be done via Customize ‣ HTML Editor which lets us read the various templates involved in rendering a page. Going through a few of them, “Product item” looks a likely culprit.
Altering view architectures is done in 3 steps:
academy/templates.xml
<div class="oe_structure"/> </t> </template> <template id="product_item_hide_no_price" inherit_id="website_sale.products_item"> <xpath expr="//div[hasclass('product_price')]/b" position="attributes"> <attribute name="t-if">product.price > 0</attribute> </xpath> </template> <!-- <template id="object"> --> <!-- <h1><t t-esc="object.display_name"/></h1> --> <!-- <dl> -->
The second thing we will change is making the product categories sidebar visible by default: Customize ‣ Product Categories lets you toggle a tree of product categories (used to filter the main display) on and off.
This is done via the customize_show and active fields of extension templates: an extension template (such as the one we’ve just created) can be customize_show=True. This choice will display the view in the Customize menu with a check box, allowing administrators to activate or disable them (and easily customize their website pages).
We simply need to modify the Product Categories record and set its default to active=”True”:
academy/templates.xml
</xpath> </template> <record id="website_sale.products_categories" model="ir.ui.view"> <field name="active" eval="True"/> </record> <!-- <template id="object"> --> <!-- <h1><t t-esc="object.display_name"/></h1> --> <!-- <dl> -->
With this, the Product Categories sidebar will automatically be enabled when the Academy module is installed.
(Quelle: mindissoftware.com und Teil2) Hier finden Sie ein Muster für die Programmierung eines Add-on-Moduls in odoo. Das Installationsskript kann leicht angepasst werden, um eine neue odoo APP aus GitHub zu installieren.
Das Beispiel hat den Namen Offene Akademie und wurde bei den odoo Open Days gezeigt. Der Quellcode von Raphael Collet wurde auch in GitHub veröffentlicht. Dieser Beitrag und das Beispielprogramm werden Sie bei der Erstellung Ihrer ersten odoo APP schnell zum Ziel führen.
Die odoo APPs in der cBUZZ OFFICE Suite definieren in der Datei /etc/odoo/openerp-server.conf den addons_path wie folgt:
addons_path = /usr/lib/python2.7/dist-packages/openerp/addons
Inside the a /usr/lib/python2.7/dist-packages/openerp/addons directory, I created an openacademy directory that has the following contents:
├── openacademy
│ ├── __init__.py # Der Einsprungpunkt für ihr Modul, hier binden sie alle weiteren Python Module ein
│ ├── __openerp__.py # Enthält verschieden Paket Informationen
│ ├── course.py # Das Modul mit ihren Objekt definitionen
│ ├── session.py # Wird unter odoo APP Teil 2 beschrieben
│ ├── static/ # Das static Verzeichnis wird angelegt, bleibt aber noch leer│ ├── view/ # Definieren Sie hier ihre Ansichten (Aktionen, Wizards, Menüs, Masken)
│ │ ├── menu.xml
mkdir /usr/lib/python2.7/dist-packages/openerp/addons/openacademy
chmod -R 755 /usr/lib/python2.7/dist-packages/openerp/addons/openacademy
./openacademy/__init__.py # Der Einsprungpunkt für ihr Modul, hier binden sie alle weiteren Python Module ein:
import course
./openacademy/__openerp__.py oder
./openacademy/__odoo__.py # the manifest file| Enthält verschieden Paket Informationen
{
'name': 'Open Academy',
'version': '1.0',
'category': 'Tools',
'summary': 'Courses, Sessions, Subscriptions',
'description': """
Courses, Sessions, Subscriptions
================================
This module manages the definition of course, the organization,
sessions, and the subscriptions of participants.
""",
'depends' : ['base'],
'data' : ['view/menu.xml'],
'images': [],
'demo': [],
'installable' : True,
'application': True,
}
This is the model class. I changed the import statement and the parent class name to match the current Odoo 8.0 version:
./openacademy/course.py # Das Modul mit ihren Objekt definitionen
from openerp import models, fields
class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string='Title', required=True)
description = fields.Text()
./openacademy/static/
./openacademy/view/menu.xml # Definiert das Menü und Maske
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<menuitem name="Open Academy" id="menu_root" sequence="110"/>
<menuitem name="General" id="menu_general" parent="menu_root"/>
<record model="ir.actions.act_window" id="action_courses">
<field name="name">Courses</field>
<field name="res_model">openacademy.course</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Courses" id="menu_courses" parent="menu_general"
sequence="1" action="action_courses"/>
</data>
</openerp>
When I re-started Odoo, it couldn't find the newly created module. I changed
all openacademy file mode to 755. Retarted Odoo, still failed.
Wenn Sie alle Dateien angelegt haben, starten Sie odoo neu:
/etc/init.d/odoo restart
Im odoo Back-End kann der Admin die lokale Open Acadamy APP jetzt installieren.
Einstellungen ‣ Benutzer ‣ Benutzer
Als Entwickler können Sie auch den Entwicklermodus aktivieren. Klicken Sie rechts oben Ihr Benutzerkonto an und wählen Sie Über odoo. Im neuen Fenster klicken Sie Entwicklermodus wählen.
This is the second part of the Open Adademy example.
In part one
we created a simple module layout. This part demonstrates model
relationships, default values and
the use of onchange api. All code are listed at the end of this blog.
When making a change of an addon code, you may need to re-start Odoo
or update the addon or do both.
To show the model relationship usage, we first create a session class.
The session class has two fields that are associated with two other models:
We can also use default=value to define default value for an attribute.
@api.onchange
is an interesting Python decorator that calls its decorated
method whenever the course attribute is changed.
Additionally, we add a sessions attribute in course model thus it can point to a list of its sessions. We changed its view file to show the list too. In the course model, the relationship of sessions is one-to-many.
Then in the Installed Modules, I was able to find Open Academy and install it. I could see the Open Academy at the top level menu, click it and then Course submenu, I was able to list courses (an empty list at the beginning) and create new courses as demonstrated by Raphael.
./openacademy/__init__.py # Der Einsprungpunkt für ihr Modul, hier binden sie alle weiteren Python Module ein:
import course
import session
./openacademy/__openerp__.py oder
./openacademy/__odoo__.py # the manifest file| Enthält verschieden Paket Informationen
{
'name': 'Open Academy',
'version': '1.0',
'category': 'Tools',
'summary': 'Courses, Sessions, Subscriptions',
'description': """
Courses, Sessions, Subscriptions
================================
This module manages the definition of course, the organization,
sessions, and the subscriptions of participants.
""",
'depends' : ['base'],
'data' : ['view/menu.xml'],
'images': [],
'demo': [],
'installable' : True,
'application': True,
}
This is the model class. In part 2 it has a new sessions attribute:
./openacademy/course.py # Das Modul mit ihren Objekt definitionen
from openerp import models, fields
class Course(models.Model):
_name = 'openacademy.course'
name = fields.Char(string='Title', required=True)
description = fields.Text()
sessions = fields.One2many('openacademy.session', 'course')
This is the new model class that shows relationships, default values and onchange api:
./openacademy/session.py # Definiert ...
from openerp import models, fields, api
class Session(models.Model):
_name = 'openacademy.session'
name = fields.Char(string='Title', required=True)
start_date = fields.Date()
duration = fields.Integer(help="Duration in days")
seats = fields.Integer(string="Number of Seats")
course = fields.Many2one('openacademy.course', required=True)
instructor = fields.Many2one('res.partner')
active = fields.Boolean(default=True)
start_date = fields.Date(default=fields.Date.today)
@api.onchange('course')
def _onchange_course(self):
if not self.name:
self.name = self.course.name
./openacademy/static/
In part 2, we added a session menu item and an action window for the session model:
./openacademy/view/menu.xml # Definiert das Menü und Maske
<openerp>
<data>
<menuitem name="Open Academy" id="menu_root" sequence="110"/>
<menuitem name="General" id="menu_general" parent="menu_root"/>
<record model="ir.actions.act_window" id="action_courses">
<field name="name">Courses</field>
<field name="res_model">openacademy.course</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Courses" id="menu_courses" parent="menu_general"
sequence="1" action="action_courses"/>
<record model="ir.ui.view" id="course_form">
<field name="name">course form view</field>
<field name="model">openacademy.course</field>
<field name="arch" type="xml">
<form string="Course" version="7.0">
<sheet>
<h1>
<field name="name" placeholder="Course Title"/>
</h1>
<notebook>
<page string="Description">
<field name="description"/>
</page>
<page string="Sessions">
<field name="sessions"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="action_sessions">
<field name="name">Sessions</field>
<field name="res_model">openacademy.session</field>
<field name="view_mode">tree,form</field>
</record>
<menuitem name="Sessions" id="menu_sessions" parent="menu_general"
sequence="2" action="action_sessions"/>
</data>
</openerp>
We didn't complete the whole demo as showed in
Odoo Open Days video: Developing V8 Backend modules by Raphael Collet because there is too much material for a tutorial.
Im nächsten Abschnitt Sie das ganze Beispiel von Raphael Collet:
Anm.: Hier finden Sie ein korrigiertes Beispiel des GitHub Beitrages von Raphael Collet.
Three-tier client/server/database
Web client in Javascript
Manage courses, sessions, and subscriptions
Inside the a /usr/lib/python2.7/dist-packages/openerp/addons directory, I created an openacademy directory that has the following contents:
├── openacademy
│ ├── __init__.py # Der Einsprungpunkt für ihr Modul und alle weiteren Python Module
│ ├── __openerp__.py # Enthält verschieden Paket Informationen
│ ├── course.py # Das Modul mit ihren Objekt definitionen
│ ├── session.py # Wird unter odoo APP Teil 2 beschrieben
│ ├── static/ # Das static Verzeichnis wird angelegt, bleibt aber noch leer│ ├── view/ # Definieren Sie Ihre Ansichten (Aktionen, Wizards, Menüs, Masken)
│ │ ├── menu.xml
mkdir /usr/lib/python2.7/dist-packages/openerp/addons/openacademy
./openacademy/__init__.py # Der Einsprungpunkt für ihr Modul, hier binden sie alle weiteren Python Module ein
import course
./openacademy/__openerp__.py oder
./openacademy/__odoo__.py # the manifest file| Enthält verschieden Paket Informationen
{
'name': 'Open Academy',
'version': '1.0',
'category': 'Tools',
'summary': 'Courses, Sessions, Subscriptions',
'description': """
Courses, Sessions, Subscriptions
================================
This module manages the definition of course, the organization,
sessions, and the subscriptions of participants.
""",
'depends' : ['base'],
'data' : ['view/menu.xml'],
'images': [],
'demo': [],
'installable' : True,
'application': True,
}
A model and its fields are defined in a Python class:
./openacademy/course.py # Das Modul mit ihren Objekt definitionen
from odoo import Model, fields class Course(Model): _name = 'openacademy.course' name = fields.Char(string='Title', required=True) description = fields.Text()
./openacademy/view/menu.xml # Definiert das Menü und Maske
<?xml version="1.0" encoding="UTF-8"?> <openerp> <data> <menuitem name="Open Academy" id="menu_root" sequence="110"/> <menuitem name="General" id="menu_general" parent="menu_root"/> <record model="ir.actions.act_window" id="action_courses"> <field name="name">Courses</field> <field name="res_model">openacademy.course</field> <field name="view_mode">tree,form</field> </record> <menuitem name="Courses" id="menu_courses" parent="menu_general" sequence="1" action="action_courses"/> </data> </openerp>
<record model="ir.ui.view" id="course_form"> <field name="name">course form view</field> <field name="model">openacademy.course</field> <field name="arch" type="xml"> <form string="Course" version="7.0"> <sheet> <h1> <field name="name" placeholder="Course Title"/> </h1> <notebook> <page string="Description"> <field name="description"/> </page> </notebook> </sheet> </form> </field> </record>
./openacademy/session.py # Definiert ...
class Session(Model): _name = 'openacademy.session' name = fields.Char(required=True) start_date = fields.Date() duration = fields.Integer(help="Duration in days") seats = fields.Integer(string="Number of Seats")
Let us link sessions to courses and instructors:
class Session(Model): _name = 'openacademy.session' ... course = fields.Many2one('openacademy.course', required=True) instructor = fields.Many2one('res.partner')
Let us back-link courses and sessions:
class Course(Model): _name = 'openacademy.course' ... responsible = fields.Many2one('res.users') sessions = fields.One2many('openacademy.session', 'course')
Let us link sessions to partners for attendee subscription:
class Session(Model): _name = 'openacademy.session' ... attendees = fields.Many2many('res.partner')
The value of those fields is computed:
class Session(Model): _name = 'openacademy.session' ... taken_seats = fields.Float(compute='_compute_taken_seats') @api.one @api.depends('attendees', 'seats') def _compute_taken_seats(self): if self.seats: self.taken_seats = 100.0 * len(self.attendees) / self.seats else: self.taken_seats = 0.0
Model instances are recordsets.
for session in self: print session.name print session.course.name assert self.name == self[0].name
Modify form values when some field is filled in:
class Session(Model): _name = 'openacademy.session' ... @api.onchange('course') def _onchange_course(self): if not self.name: self.name = self.course.name
Specify the initial value to use in a form:
class Session(Model): _name = 'openacademy.session' ... active = fields.Boolean(default=True) start_date = fields.Date(default=fields.Date.today) ...
Prevent bad data:
from odoo.exceptions import Warning class Session(Model): _name = 'openacademy.session' ... @api.one @api.constrains('instructor', 'attendees') def _check_instructor(self): if self.instructor in self.attendees: raise Warning("Instructor of session '%s' " "cannot attend its own session" % self.name)
Modules have a simple structure
To let my code find other odoo modules, I added the following line to the .profile of odoo user account (the account used to run Odoo).
(Quelle: Odoo) Seit der Version V8 verkürzt odoo den Prozess zur Erstellung von odoo Frontend-Modulen (zB. Internet Websites) die leicht mit den odoo Back-End-Modulen (zB. ERP, CRM, Buchhaltung) zusammenarbeiten können.
Der Ausdruck Front-End und Back-End wird hier aus der Sicht des odoo Anwenders verwendet.
Odoo kann aber auch einfach als Standalone-Web Application Framework eingesetzt werden - ähnlich wie Djangohttps://www.djangoproject.com/ oder Flaskhttp://flask.pocoo.org/ - ohne den Back-End-Teil zu berühren.
Die besondere Stärke von odoo ist es aber, ein modernes Web Application Framework zu sein, dass letztlich die Entwicklungszeit für Projekte stark reduziert, die sowohl Back-End als auch Front-End-Funktionen erfordern. In odoo braucht dafür keine spezielle Middleware (Konnektoren) mehr programmiert werden, damit die einzelnen Systemen miteinander kommunizieren können.
Wie alle modernen Web-Applikations-Frameworks verwendet auch odoo das MVC Modell (Model View Controller) als Basis für die Strukturierung sowohl der Front-End-Module als auch der Back-End-Module. In diesem Beitrag werden wir zeigen, wie der Controller und die Front-End-Ansichten (Views als Qweb Vorlagen) zusammen arbeiten. Später werden wir Sie bei der Erstellung eines kompletten Frontend-Moduls in odoo begleiteten, indem wir eine einfache Blog-Anwendung erstellen.
Der "MVC Modell" Bereich wird in diesem Beitrag nicht behandelt, alle Interessierten finden dazu aber andere Ressourcen im Web. Wie das neue Modell API funktioniert wurde bereits bei den OpenDays 2014https://github.com/nbessi/odoo_new_api_guideline beschrieben und wird auf odoo's Website zu Technische Dokumentationhttps://www.odoo.com/documentation/8.0/ ständig ergänzt.
In Web-Anwendungen sind die Controller einfach HTTP-Handler, die definieren was passiert, wenn in einer HTTP-Anfrage ein "Treffer" auf einen bestimmten Pfad auf unserem Server zeigt. Odoo verwendet die werkzeughttp://werkzeug.pocoo.org/ Bibliothek für die Umsetzung des Controllers. Sie können also in der Dokumentation zur werkzeug5 Bibliothek jederzeit nachlesen, um die volle Leistungsfähigkeit der Bibliothek nutzen zu können.
Einen kurzen Überblick über die Controller in odoo finden Sie mit diesen einfachen Beispielen.
Hier ist einfaches Beispiel eines Controllers (einer Steuerung), die nur die Nachricht "Hallo Welt" ausgibt:
from openerp.http import Controller, route
class TestControllers(Controller):
@route("/hello/")
def hello(self):
return "Hello World"
As
we can see the controller is just a method of a class which inherits
from openerp.http.Controller and is wrapped using the decorator function
openerp.http.route.
If we open our browser and go to ‘localhost:odoo_port/hello/’ we will see our message.
The returned string is wrapped inside a proper HTML document so you can put there any valid HTML and it will be rendered correctly in the browser. Example:
@route("/hello/")
def hello(self):
return "<strong>Hello World</strong>"
This will make the previous message bold as expected.
Routes can also be specified as patterns and the variable parts will be supplied as parameters to the controller method. Example:
@route("/hello/<name>/")
def hello(self, name):
return "Hello %s!" % name
These arguments will always be represented as strings in Python but if you know that the supplied parameter needs to be something other than a string we can use convertors. Example:
@route("/double/<int:number>/")
def double(self, number):
return "%d * 2 = %d" % (number, number * 2)
The converters are extendable you can create your own if you like. There is more info about them in the werkzeug docs.
Odoo has defined its own converter for the Model Records. Here is an example:
@route("/user/<model('res.users'):user>/")
def user_name(self, user):
return user.name
You can test it out by going to this route /user/res-users-1-1/ You should see the name of the Administrator user.
We can also control the access for a specific controller by using the auth keyword parameter on the route decorator. Example:
@route("/hello/<name>/", auth="user")
def hello(self, name):
return "Hello %s!" % name
There are three possible values for auth: user, public, none. When setting auth to user it means that the user must be logged in to access the controller. This is the default value, so when we don’t specify the auth parameter user authentication is used. If we set it to public it means that even not logged in users can access the controller. If the user is not logged in then he will be assigned to the Public user group. So we can specify different access rights for these users if we like.
For example if there is possibility to enter some data for some model using that controller, we can give the Public user group only read rights on that model and they will have to log in if they want to enter some data otherwise they will get a 403 HTTP response code. The other option is to use ‘none’ which just means that if the user is not logged in he wont be assigned to any group and the controller can be accessed by anyone, same as for public.
Before v8 if you have tried to develop web apps in Odoo you had to integrate it yourself with some existing templating system like Jinja or Mako. But QWeb templates really facilitate what Odoo is great for. That is it allows developers, or even users in this case, to easily change what other developers before them have created. This is because the QWeb templates are XML documents which are stored in the database, same as the other back-end views in Odoo, so they can be changed even after they have initially been imported/created.
Vor der Version odoo v8 konnte man Web-Anwendungen in odoo nur entwickeln, wenn man selbst Template-Systeme wie Jinja oder Mako integrierte. Mit den Qweb Vorlagen geht odoo v8 einen neuen großartigen Schritt. Jetzt können Entwickler und sogar Anwender auf einfache Weise ändern, was andere Entwickler vor ihnen geschaffen haben. Die odoo Qweb Vorlagen sind einfach XML-Dokumente, die in der Datenbank gespeichert werden, wie die anderen odoo Back-End-Ansichten auch. Nachdem die Vorlagen zunächst importiert/erstellt wurden, können sie jederzeit und rasch verändert werden.
This is a different approach from other templating systems where the template files are basically just static files which you can’t really change that much except in places where the original developer had made possible.
Das ist ein anderer Ansatz, als der von anderen Template-Systeme bekannte Ansatz, bei denen die Vorlagendateien im Grunde nur statische Dateien sind, die Sie nicht wirklich einfach können, außer an den Stellen, wo der ursprüngliche Entwickler die Vorlagen gespeichert hat.
We are not going to cover here how to inherit and change other templates but it is very similar to how view inheritance is done for the back-end views, that is just supply an inherit_id and use XPath to change the inherited template.
To define a QWeb template we just create the following XML structure:
<template id="template_id" name="Sample Template">
...
</template>
To use a template as the response from a controller, instead of just returning the naked HTML string, we need to make a call to the request.render method.
# Controller
@route("/hello//")
def hello(self, name):
return request.render("module_name.template_id", {'name': name})
<!--Template-->
<template id="template_id" name="...">
Hello <t t-esc="name" />!
</template>
<!--GET /hello/Jane-->
<!--Result: Hello Jane!-->
The first parameter that is passed to the method is the id of the template which must always be classified with the module name where our template is defined. The other parameter is a dictionary where we can define variables that can later be called from within the template itself.
I’m going to give a short overview of the QWeb syntax.
QWeb uses “magical” XML tag attributes to control how the templates are rendered. We can divide these in four groups: output, control flow, functional and assignment attributes.
Output attributes are used to render some value in the template:
t-raw, renders the value of a python expression. Example:
<t t-raw="'Hello' + '!' * 3" />
<!--Result: Hello!!!-->
t-esc, same as t-raw but it does HTML escaping on the final value of the expression. This one is safer and should be used always when rendering values that can be provided from users of the website because it will prevent the possibility of cross-stie scripting (XSS) attacks.
t-att-*, used to dynamically generate the value of an XML tag attribute. The * can be substituted for any attribute, like for example class, id, name, etc. It is also possible to use the t-attf-* attribute which uses special formattingsyntax that is very similar of how Jinja renders variables. Example:
# Controller
# ...
post_id = 1
request.render(template_id, {"post_id": post_id})
<!--Template-->
<div t-att-id="'post-' + str(post_id)" />
<!--or simpler-->
<div t-attf-id="post-{{ post_id }}" />
<!--Result: <div id="post-1" />-->
Control flow attributes which enable to do branching and looping within the template.
t-if, behaves exactly as the python if statement and if it evaluates to True then the tag and all subtags will be rendered. There is no construct for the else statement but this can easily be simulated by doing a negation (not) of the expression from another t-if attribute. Example:
# Controller
# ...
request.render(template_id, {"test": True})
<!--Template-->
<t t-if="test">
First condition
</t>
<t t-if="not test">
Else condition
</t>
<!--Result: First condition-->
t-foreach and t-as, these two are used to define a for loop. For example:
<t t-foreach="[1,2,3]" t-as="digit">
<t t-esc="digit" />
</t>
<!--Result: 1 2 3-->
It is possible to also define new variables within the template context using the assignment attribute t-set and t-value. Example:
<t t-set="message" t-value="'Hello' + '!!!'" />
<t t-esc="message" />
<!--Result: Hello!!!-->
t-valuef can also be used here and using the previous example it should look like this:
<t t-set="message" t-valuef="Hello{{ !!! }}" />
<t t-esc="message" />
<!--Result: Hello!!!-->
As you have noticed in the examples there is also a special tag <t/> which can be used if we don’t want the result nested within some tag since the <t/> tag will be omitted from the resulting HTML output. But we can also use any other tag and the result will be nested inside it:
<div t-set="message" t-valuef="Hello{{ !!! }}" />
<div t-esc="message" />
<!--Result: <div>Hello!!!</div>-->
We can also use templates as functions and call them from within other templates. They will be rendered in the place where the template was called. The functional attributet-call is used to call templates and the value of this attribute should be a fully qualified template id. Example:
# Controller
# ...
request.render(module_name.main, {})
<!--Template-->
<template id="message" name="...">
<strong>Hello!!!</strong>
</template>
<template id="main" name="..." />
<div class=”wrapper”>
<t t-call="module_name.message">
</div>
</template>
<!--Result: <div class=”wrapper”><strong>Hello!!!</strong></div>-->
Now that we have the basics down, we can start with making a complete web app in Odoo.
Die odoo APPs in der cBUZZ OFFICE Suite definieren in der Datei /etc/odoo/openerp-server.conf den addons_path wie folgt:
addons_path = /usr/lib/python2.7/dist-packages/openerp/addons
Then inside the a /usr/lib/python2.7/dist-packages/openerp/addons directory, I created an odoo_blog directory that has the following contents:
To start we should create this directory structure in our file system.
├── odoo_blog│ ├── __init__.py # Der Einsprungpunkt für ihr Modul, hier binden sie alle weiteren Python Module ein
│ ├── static/ # Bleibt leer
│ ├── __openerp__.py # Enthält verschieden Paket Informationen
│ ├── model│ │ ├── __init__.py
│ │ ├── post.py
│ └── view # Definieren Sie hier ihre Ansichten (Aktionen, Wizards, Menüs, Masken)
│ │ ├── posts.xml
│ ├── controller # Definieren Sie hier Ihre Objekte
│ │ ├── __init__.py
│ │ ├── posts.py
The __openerp__.py file should something look like this:
{
# ...
'description': """
A simple blog application that allows
for creating, editing, deleting and viewing
blog posts.
""",
'depends': [
'website',
],
'installable': True,
'application': False,
}
The important thing to note here is that front-end modules should depend on the website module. It is possible also to develop front-end modules or standalone web applications without inheriting from website, since the web application framework capabilities are all in the base core odoo module.
It is advisable to inherit from website whenever you want create front-end modules which are more closely integrated with the back-end. But even if the module inherits from website you are still able to control which controllers (no pun intended) use website.
Now that we have the proper skeleton Odoo module structure in place we can start by creating our model which will hold the the necessary data for our blog.
In our simple application a blog is just a collection of posts so we can start by creating a model that will hold data for them. Create a file in the model directory and name it post.py. Here is how it should look:
#file: odoo_blog/model/post.py
from openerp.models import BaseModel
from openerp import fields
class Post(BaseModel):
_name = 'blog.post'
title = fields.Char('Name')
content = fields.Text('Content')
This model doesn’t implement any methods because for our simple application the standard methods like search, write and unlink from the inherited BaseModel are enough.
After we have created our model we can start working on our HTTP controllers. This is how the controller for listing all posts should look like:
#file: odoo_blog/controller/posts.py
from openerp.http import Controller, route, request
class Posts(Controller):
@route('/posts/', website=True)
def list(self):
posts = request.env['blog.post']
posts = posts.search([])
return request.render('odoo_blog.posts_list',
{'posts': posts})
The first thing that we can see is the decorator @route used on the method. The only required argument for the decorator is of course the route/url path itself which binds to this controller.
The keyword argument website in the example is optional as we noted earlier and should be used only when the module that you are working on depends on the website module.
The body of the controller method is also very simple and uses the new Odoo model api, which we are not going to cover in this post. So in short the first two statements just retrieve all the posts that are already created. At the end we just call the posts_list template and we pass to its context all the posts that are entered so far.
The next task is to create a view / template for the list controller (/posts) that we described earlier.
Here is how it should look:
<!--file: odoo_blog/view/posts.xml-->
<template id="posts_list" name="Blog Posts">
<t t-call="website.layout">
<div class="row">
<div class="col-md-10">
<div class="col-md-2">
<a href="/posts/new/" class="btn btn-primary pull-right" role="button">Create</a>
</div>
<t t-foreach="posts" t-as="post">
<t t-call="odoo_blog.post_render"/>
</t>
</div>
</div>
</t>
</template>
Of course the __openerp__.py should be updated also with the template path.
{
# ...
'data': [
'view/posts.xml'
]
# ...
}
The first thing that we notice is that a t-call is made to the website.layout template. This is one of the main benefits of using the website module. That is we can use the already existing templates there which already integrate the front-end and the back-end part of Odoo. The body of the t-call will be set to the “magic” variable 0. We can see the place where the contents of the t-call body will be rendered in this sample from the official website module:
<!--file: website/views/webiste_templates.xml-->
<template id="layout" name="Main layout">
...
<body>
...
<main>
<t t-raw="0"/> <!--Your HTML is injected here-->
</main>
...
</body>
...
</template>
The website.layout template uses bootstrap3 so we use it to style our templates as well. Another thing is the “a” link tag which points to the posts/new route. This will be another controller which we will implement later. Next we can see a for loop on the posts RecordSet and inside i it we make another t-call to a template within our own module. This is how that template looks:
<template id="post_render" name="Post">
<div class="media">
<div class="media-body">
<h4 class="media-heading" t-esc="post.title" />
<p t-esc="post.content" />
<div class="btn-group">
<a t-attf-href="/posts/{{ slug(post) }}/view/" class="btn btn-default btn-xs" role="button">View</a>
<a t-attf-href="/posts/{{ slug(post) }}/edit/" class="btn btn-default btn-xs" role="button">Edit</a>
<a t-attf-href="/posts/{{ slug(post) }}/delete/" class="btn btn-default btn-xs" role="button">Delete</a>
</div>
</div>
</div>
</template>
Inside this template the title and the content of the post are rendered using the t-esc attribute.
We can see there that we use the post variable to access the post data
and we haven’t defined anywhere in this template what post actually is.
This works because the template is called from another template which
defined post, in the for loop we saw. So the template context is passed
from one template to another when we make a t-call and we can use the
called template just like a function by setting the value of the defined
“parameters” inside it.
The link tags, or buttons, point to controllers that we haven’t defined yet. We can see from this tags how we dynamically set the href attribute by using the t-attf-href form and substitute inside it the post url slug. The slug function is passed insider the template context whenever you use the website model, website=True, so you just pass in it an Object/Record and it will give the correct url slug which should be used whenever you use the Model converter in the controller route path.
So now we can finally test what we have done so far by updating our module first (restart the server also) and then point the our browser to /posts. We should see the a navigation menu and also some default text in the footer. This is all defined by the website.layout template which we are calling. So we can’t see any posts, this is because we haven’t created any so far.
Now we need to create the controller for handling the creation of posts. Here is how it should look:
@route('/posts/new/', website=True)
def new(self, **form_data):
form = PostsNewForm(request.httprequest.form)
if request.httprequest.method == 'POST' and form.validate():
posts = request.env['blog.post']
posts.create({
'title': form_data.get('title', ''),
'content': form_data.get('content', ''),
})
return redirect("/posts/")
return request.render('odoo_blog.posts_new', {'form': form})
from wtforms import Form, StringField, TextAreaField, validators
class PostsNewForm(Form):
title = StringField('Title', [validators.Required()])
content = TextAreaField('Content', [validators.Required()])
And the templates:
<template id="posts_new" name="New Post">
<t t-call="website.layout">
<div class="row">
<div class="col-md-6 col-md-offset-2">
<form role="form" action="/posts/new" method="POST">
<t t-foreach="form" t-as="field">
<t t-call="odoo_blog.field_render" />
</t>
<button type="submit" class="btn btn-primary mt8">Save</button>
</form>
</div>
</div>
</t>
</template>
<template id="field_render" name="Render Form Field">
<div t-attf-class="form-group {{ field.errors and 'has-error' or '' }}">
<t t-raw="field.label" />
<t t-raw="field(class_='form-control')" />
<span t-foreach="field.errors" t-as="error" class="help-block">
<t t-esc="error" />
</span>
</div>
</template>
This controller is more interesting because it has to deal with both GET and POST requests.
When we go to the Create New Post page we can see an HTML form there.
This is the result of the GET request sent automatically by the browser
when we try to open a page. So the handler just displays the empty form
then. But when we try to Save the data in the form by creating a new
post we will send a POST HTTP request to the same controller. So the
controller needs to distinguish between these two types of requests and
also validate the data sent by the form. If the data is correct then it
will create a new post from it and redirect the user to /posts page.
Form validation is a very common thing when creating web apps so we are
using the wtforms library for this task. It is very easy and intuitive
to use so you can check their documentation to learn the basics.
The form object is always passed to the template and by using it we can render the HTML form and also show if any errors were caught when the user tried to create a post. We have defined a separate template, field_render, that deals with rendering of the form fields. It is fairly easy to understand and basically the field knows how to render itself so we can just call it and pass any attribute that we wish to change in its HTML, in our case we want the field to have the bootstrap class form-control so that it displays properly. Just note that we are using t-raw for getting the value of the field because we don’t want the HTML to be escaped as it would be if we used t-esc.
So now we have the ability to create and list all posts. We just need to implement the Edit and Delete handlers and we will have our basic CRUD Blog application. But we are going to leave this part for you and if you like you can always check our git repository that we made for this example.
Fehlermeldung: Bei deinstallieren eines Moduls kommt die Meldung:
Integrity Error The operation cannot be completed, probably due to the following: - deletion: you may be trying to delete a record while other records still reference it - creation/update: a mandatory field is not correctly set [object with reference: ir.model.constraint - ir.model.constraint]
The reason of this behaviour is as follows,
Earlier you might have added one referential field and restarted the server so that field ( for example, Many2One ) created with constraint of foreign key in database. Now for some reason you changed the column name or delete that column, so the reference of that constraint is still there in postgresql database.
Now when you going to uninstall the module, system will go to clear ir.model & ir.model.fields entries, which are belongs to that module. But the column which is not available in py but constraint will be there under ir.model.constraint model.
The solution of this could be,
It should be work now unless you are fail to explain your problem clearly. I am sure this will help you!