API της σουίτας l10n.gr για συνεργάτες
Ό,τι προσθέτει η σουίτα l10n.gr στο Odoo και μπορείτε να χρησιμοποιήσετε στα δικά σας modules: μοντέλα που κάνετε inherit, πεδία που διαβάζετε ή γράφετε, μέθοδοι που καλείτε και hooks που κάνετε override με super(). Για όλα αυτά δεσμευόμαστε ότι μένουν συμβατά από έκδοση σε έκδοση.
Πώς τα χρησιμοποιείτε
- Στο __manifest__.py του module σας προσθέτετε στο depends το module του l10n.gr που χρησιμοποιείτε. Το όνομά του αναγράφεται δίπλα στον τίτλο κάθε ενότητας. Έτσι το Odoo φορτώνει το module σας μετά από αυτό.
- Τα μοντέλα, τα πεδία και τις μεθόδους του l10n.gr τα χρησιμοποιείτε μέσα από το ORM: self.env['l10n_gr.doy'], _inherit = 'account.move', record.l10n_gr_doy_id. Στον κώδικά σας χρειάζονται μόνο τα συνηθισμένα imports του Odoo, όπως from odoo import api, fields, models.
- Μην κάνετε import από odoo.addons.l10n_gr_*. Ο κώδικας της σουίτας δεν είναι δημόσιο API, και το pipeline επισημαίνει κάθε τέτοιο import.
Οτιδήποτε δεν αναφέρεται εδώ είναι εσωτερικό της σουίτας και μπορεί να αλλάξει χωρίς προειδοποίηση. Οι διαβιβάσεις, η αρίθμηση των σειρών και τα ΜΑΡΚ/UID/QR προστατεύονται, και το pipeline μπλοκάρει κάθε override τους.
Θέλετε να δοκιμάζετε τον κώδικά σας στο staging χωρίς να περιμένετε το pipeline; Ζωντανή ανάπτυξη με l10n-gr dev →
Πίνακες Αναφοράς ΑΑΔΕ
depends: ['l10n_gr_nt_aade_tables']
Οι επίσημοι πίνακες κωδικών της ΑΑΔΕ (myDATA), διαθέσιμοι ως models του Odoo (κατηγορίες ΦΠΑ, αιτίες εξαίρεσης, τύποι παραστατικών, χαρακτηρισμοί εσόδων και εξόδων, σκοποί διακίνησης, παρακρατούμενοι φόροι), και τα fields που τους συνδέουν με τους φόρους.
aade.expense.category
model aade.expense.category (myDATA table 8.10): code Char (required), name Char (required), l10n_gr_allow_null_e3 Boolean (default False), display_name Char (stored)Κατηγορία χαρακτηρισμού εξόδων (myDATA 8.10). Οι κατηγορίες εξόδων της ΑΑΔΕ (category2_1, category2_5, ...), με την ίδια ένδειξη l10n_gr_allow_null_e3 που έχει και ο πίνακας εσόδων. Χρησιμοποιήστε τον ως δεδομένα αναφοράς σε fields και φίλτρα των αγορών.
Παράδειγμα
Ο λογιστής θέλει κάθε κατηγορία ειδών να έχει σταθερή κατηγορία εξόδου myDATA, ώστε οι αγορές της να χαρακτηρίζονται πάντα με τον ίδιο τρόπο.
# Customer need: each product category books its purchases under one myDATA
# expense category, chosen once by the accountant.
# depends: ['l10n_gr_nt_aade_tables']
# The suite tables are reached through the ORM, never imported from odoo.addons.
from odoo import _, api, fields, models
from odoo.exceptions import ValidationError
class ProductCategory(models.Model):
_inherit = 'product.category'
x_expense_category_id = fields.Many2one(
'aade.expense.category', string='myDATA expense category',
domain=[('l10n_gr_allow_null_e3', '=', False)])
@api.constrains('x_expense_category_id')
def _check_x_expense_category(self):
# the domain guards the form only; imports go through this check
for categ in self.filtered('x_expense_category_id'):
if categ.x_expense_category_id.l10n_gr_allow_null_e3:
raise ValidationError(_('%s is valid without an E3 code: choose a category for regular purchases.',
categ.x_expense_category_id.display_name))aade.expense.type
model aade.expense.type (myDATA table 8.11): code Char (required, E3 code), name Char (required), display_name Char (stored)Τύπος χαρακτηρισμού εξόδων / κωδικός Ε3 (myDATA 8.11). Οι κωδικοί Ε3 των γραμμών εξόδων. Είναι δεδομένα αναφοράς μόνο για ανάγνωση· χρησιμοποιήστε τα σε δικές σας αναφορές ή σε fields των αγορών.
Παράδειγμα
Ένας πελάτης που μεταφέρεται από παλαιό ERP χρειάζεται να αντιστοιχίσει τους δικούς του κωδικούς εξόδων στους επίσημους κωδικούς Ε3 πριν καταχωριστεί η πρώτη αγορά στο Odoo.
# Customer need: the legacy ERP coded costs with its own codes; before go-live,
# map each one to the E3 code the accountant chose.
# Run in odoo-bin shell from the console (env is given). x_legacy_code and
# x_expense_type_id are your own fields on product.category;
# your module's depends: ['l10n_gr_nt_aade_tables']
MAPPING = { # legacy cost code -> E3 code, as agreed with the accountant
'K100': 'E3_102_001',
'K200': 'E3_102_002',
}
E3 = env['aade.expense.type']
for legacy_code, e3_code in MAPPING.items():
e3 = E3.search([('code', '=', e3_code)], limit=1)
if not e3:
print('Unknown E3 code', e3_code, 'for', legacy_code)
continue
categs = env['product.category'].search([('x_legacy_code', '=', legacy_code)])
categs.write({'x_expense_type_id': e3.id})
print(legacy_code, '->', e3.display_name, '(%s categories)' % len(categs))
env.cr.commit()aade.income.category
model aade.income.category (myDATA table 8.8): code Char (required, e.g. 'category1_1'), name Char (required), l10n_gr_allow_null_e3 Boolean (default False), display_name Char (stored)Κατηγορία χαρακτηρισμού εσόδων (myDATA 8.8). Οι κατηγορίες εσόδων της ΑΑΔΕ (category1_1, category1_2, ...). Το l10n_gr_allow_null_e3 σημειώνει τις πληροφοριακές κατηγορίες που ισχύουν χωρίς κωδικό Ε3. Χρησιμοποιήστε τον ως comodel στα δικά σας Many2one fields ή για να διαβάσετε την κατηγορία που έχει οριστεί ειδικά σε ένα είδος (product.template.l10n_gr_income_category_id).
Παράδειγμα
Μια επιχείρηση που πουλά χονδρικά και μέσω e-shop θέλει κάθε ομάδα πωλήσεων να συνδέεται με την κατηγορία εσόδου myDATA που της αντιστοιχεί, ώστε τα έσοδα να ελέγχονται ανά κατηγορία.
# Customer need: the e-shop team and the wholesale team must be reported under
# their own myDATA income category; the accountant groups invoices by it.
# depends: ['l10n_gr_nt_aade_tables', 'sale']
# The suite tables are reached through the ORM, never imported from odoo.addons.
from odoo import fields, models
class CrmTeam(models.Model):
_inherit = 'crm.team'
x_income_category_id = fields.Many2one(
'aade.income.category', string='myDATA income category',
domain=[('l10n_gr_allow_null_e3', '=', False)])
class AccountMove(models.Model):
_inherit = 'account.move'
# stored, so the accountant can group and pivot invoices by it
x_income_category_id = fields.Many2one(
related='team_id.x_income_category_id', store=True,
string='Team income category')aade.income.type
model aade.income.type (myDATA table 8.9): code Char (required, E3 code), name Char (required), display_name Char (stored)Τύπος χαρακτηρισμού εσόδων / κωδικός Ε3 (myDATA 8.9). Οι κωδικοί Ε3 των γραμμών εσόδων (π.χ. E3_561_001). Διαβάστε τον όταν το module σας πρέπει να εμφανίσει τον κωδικό Ε3 μιας κατηγορίας εσόδων ή να φιλτράρει με αυτόν.
Παράδειγμα
Κατά τη μετάπτωση από παλαιό ERP, ο συνεργάτης τηρεί έναν πίνακα αντιστοίχισης, ελεγμένο από τον λογιστή, που μετατρέπει κάθε παλαιό κωδικό εσόδου σε κατηγορία εσόδου myDATA και κωδικό Ε3.
# Customer need: migrating from a legacy ERP whose revenue codes must become a
# myDATA category + E3 code; keep the mapping as data the accountant can review.
# depends: ['l10n_gr_nt_aade_tables'] (add an ir.model.access line for acme.income.map)
from odoo import fields, models
class AcmeIncomeMap(models.Model):
_name = 'acme.income.map'
_description = 'Legacy revenue code to myDATA classification'
legacy_code = fields.Char(required=True, index=True)
category_id = fields.Many2one('aade.income.category', required=True)
type_id = fields.Many2one('aade.income.type', string='E3 code')
e3_code = fields.Char(related='type_id.code')
_legacy_code_uniq = models.Constraint('UNIQUE(legacy_code)', 'One mapping per legacy code.')
def _incomplete(self):
# categories valid without an E3 code may go without one; all others need one
return self.filtered(
lambda m: not m.type_id and not m.category_id.l10n_gr_allow_null_e3)aade.invoice.type
model aade.invoice.type (myDATA table 8.1): code Char (required), name Char (required), display_name Char (stored), sequence, active, book Selection, issuer_role Selection, flow Selection, supported Boolean (stored compute), variant_ids, correlated_type_ids, correlation_required, provider_required and per-slot rule Selections; ordered by sequence, codeΤύπος παραστατικού (myDATA 8.1). Μία εγγραφή ανά τύπο παραστατικού της ΑΑΔΕ (1.1, 2.1, 5.1, 11.1 κ.λπ.) με τους κανόνες του: σε ποιο βιβλίο καταλήγει, ποιος τον εκδίδει, σε ποια ροή του Odoo ανήκει, αν εκδίδεται σήμερα, ποιες παραλλαγές επιτρέπονται και με ποιους τύπους συσχετίζεται. Διαβάστε τον όταν η λογική σας πρέπει να διαφέρει ανάλογα με τον τύπο παραστατικού ενός ημερολογίου ή μιας κίνησης, π.χ. code == '11.1' για αποδείξεις λιανικής. Μη δημιουργείτε και μην αλλάζετε τύπους.
Παράδειγμα
Στο κλείσιμο του μήνα ο λογιστής θέλει έναν πίνακα με το πλήθος των παραστατικών κάθε τύπου ΑΑΔΕ που εκδόθηκαν, για να τον συγκρίνει με τη σύνοψη του myDATA.
# Customer need: the accountant's month-end dashboard counts the documents of
# each AADE type (1.1, 2.1, 11.1 ...) posted in the period.
# depends: ['l10n_gr_nt_chart_ext'] (journal field; brings l10n_gr_nt_aade_tables)
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def acme_documents_by_type(self, date_from, date_to):
groups = self._read_group(
[('state', '=', 'posted'), ('company_id', '=', self.env.company.id),
('date', '>=', date_from), ('date', '<=', date_to),
('journal_id.l10n_gr_invoice_type_id', '!=', False)],
['journal_id'], ['__count'])
counts = {}
for journal, count in groups:
# read only: never create or edit document types
doc_type = journal.l10n_gr_invoice_type_id
counts[doc_type] = counts.get(doc_type, 0) + count
# keyed by record, so a type archived after posting is still counted
return [(t.code, t.name, n) for t, n in
sorted(counts.items(), key=lambda kv: (kv[0].sequence, kv[0].code))]aade.move.purpose
model aade.move.purpose (myDATA table 8.14): code Integer (required, indexed), name Char (required), active Boolean (default True), scope Selection [internal, external, both] (required, default both); ordered by codeΣκοπός διακίνησης (myDATA 8.14). Οι σκοποί διακίνησης αγαθών στα δελτία αποστολής (πώληση, μεταφορά μεταξύ εγκαταστάσεων της ίδιας επιχείρησης, επιστροφή, ...). Το scope είναι κανόνας της σουίτας για το UI και όχι της ΑΑΔΕ: ορίζει αν ένας σκοπός προσφέρεται σε εσωτερικές μεταφορές, σε διακινήσεις προς ή από τρίτους ή και στα δύο. Διαβάστε τον για να φιλτράρετε σκοπούς στους δικούς σας wizards· ο λογιστής μπορεί να αλλάξει το scope και το active από το UI.
Παράδειγμα
Το προσωπικό της αποθήκης που μεταφέρει εμπορεύματα μεταξύ των εγκαταστάσεων της εταιρείας πρέπει να βλέπει μόνο τους σκοπούς διακίνησης που ισχύουν για εσωτερικές μεταφορές.
# Customer need: the warehouse moves stock between the customer's own branches
# and must pick a movement purpose; offer only purposes for internal moves.
# depends: ['l10n_gr_nt_aade_tables', 'stock'] (add an ir.model.access line for the wizard)
from odoo import _, fields, models
class AcmeBranchTransfer(models.TransientModel):
_name = 'acme.branch.transfer'
_description = 'Transfer between own branches'
picking_id = fields.Many2one('stock.picking', required=True)
purpose_id = fields.Many2one(
'aade.move.purpose', required=True,
# the accountant sets scope and active from the UI: filter, never hard-code ids
domain=[('scope', 'in', ('internal', 'both'))])
def action_confirm(self):
self.ensure_one()
self.picking_id.message_post(
body=_('Movement purpose: %(code)s %(name)s',
code=self.purpose_id.code, name=self.purpose_id.name))
return {'type': 'ir.actions.act_window_close'}aade.vat.category
model aade.vat.category (myDATA table 8.2): code Integer (required), name Char (required), percent Float, display_name Char (stored compute, '[code] name'); ordered by codeΚατηγορία ΦΠΑ (myDATA 8.2). Πίνακας αναφοράς με τις κατηγορίες ΦΠΑ της ΑΑΔΕ, μία εγγραφή ανά κωδικό με τον συντελεστή του. Χρησιμοποιήστε τον για να βρείτε την κατηγορία ενός συντελεστή ή για να φιλτράρετε φόρους ανά κατηγορία στο δικό σας module. Είναι δεδομένα μόνο για ανάγνωση: οι εγγραφές έρχονται με το module και ακολουθούν τον πίνακα της ΑΑΔΕ. Απαιτεί εγκατεστημένο το l10n_gr_nt_aade_tables.
Παράδειγμα
Ο λογιστής χρειάζεται τις καθαρές πωλήσεις του μήνα ανά κατηγορία ΦΠΑ του myDATA, για να τις συμφωνήσει με την περιοδική δήλωση ΦΠΑ.
# Customer need: a monthly dashboard of sales net value per VAT category, so the
# accountant can cross-check it with the VAT return.
# depends: ['l10n_gr_nt_aade_tables']
from odoo import models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def acme_sales_by_vat_category(self, date_from, date_to):
base_domain = [
('parent_state', '=', 'posted'), ('company_id', '=', self.env.company.id),
('move_id.move_type', 'in', ('out_invoice', 'out_refund')),
('date', '>=', date_from), ('date', '<=', date_to),
('display_type', '=', 'product')]
result = []
for cat in self.env['aade.vat.category'].search([]): # ordered by code
lines = self.search(base_domain + [('tax_ids.aade_vat_category_id', '=', cat.id)])
if lines:
# sales are credits: negate the balance to show positive net value
result.append((cat.display_name, cat.percent, -sum(lines.mapped('balance'))))
return resultaade.vat.exemption.category
model aade.vat.exemption.category (myDATA table 8.3): code Integer (required), name Char (required), name_n5144 Char, l10n_gr_foreign_vat_line Boolean; ordered by codeΑιτία εξαίρεσης ΦΠΑ (myDATA 8.3). Πίνακας αναφοράς με τις αιτίες εξαίρεσης ΦΠΑ της ΑΑΔΕ. Το name κρατά την αιτία με την αρίθμηση του προηγούμενου Κώδικα ΦΠΑ και το name_n5144 τη διατύπωση του ν. 5144/2024 όπου διαφέρει· το display_name δείχνει τον κωδικό, την τρέχουσα διατύπωση και το παλιό άρθρο. Το l10n_gr_foreign_vat_line σημειώνει τις αιτίες όπου ο φόρος της γραμμής είναι ΦΠΑ άλλου κράτους μέλους (OSS). Διαβάστε τον για να εμφανίσετε ή να ελέγξετε την αιτία εξαίρεσης ενός φόρου· μην αλλάζετε τις εγγραφές.
Παράδειγμα
Ένας πελάτης που αγοράζει με απαλλαγή ΦΠΑ βάσει συγκεκριμένης διάταξης δεν πρέπει ποτέ να λάβει επιβεβαιωμένη παραγγελία με φόρο που αναφέρει διαφορετικό λόγο απαλλαγής.
# Customer need: a shipping customer buys VAT-free under an exemption certificate;
# block orders whose taxes carry a different exemption reason.
# depends: ['l10n_gr_nt_aade_tables', 'sale']
from odoo import _, fields, models
from odoo.exceptions import UserError
class ResPartner(models.Model):
_inherit = 'res.partner'
x_vat_exemption_id = fields.Many2one('aade.vat.exemption.category', string='VAT exemption reason')
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
for order in self:
reason = order.partner_id.commercial_partner_id.x_vat_exemption_id
if not reason:
continue
wrong = order.order_line.filtered(
lambda l: not l.display_type and reason not in l.tax_ids.aade_vat_exemption_category_id)
if wrong:
raise UserError(_('%(lines)s: use a tax with exemption %(reason)s.',
lines=', '.join(wrong.mapped('product_id.display_name')),
reason=reason.display_name))
return super().action_confirm()aade.vat.exemption.category.l10n_gr_printed_reason
l10n_gr_printed_reason(self) -> str (ensure_one)Ένδειξη απαλλαγής για το έντυπο. Επιστρέφει την ένδειξη απαλλαγής που πρέπει να τυπωθεί στο τιμολόγιο: τη διατύπωση του ν. 5144/2024 όταν υπάρχει, αλλιώς το αρχικό name. Καλέστε τη σε μία εγγραφή (σε σύνολο εγγραφών σηκώνει σφάλμα) όταν φτιάχνετε δική σας αναφορά ή QWeb template, ώστε το έντυπο να μην αναφέρει καταργημένο άρθρο.
Παράδειγμα
Η ειδική εκτύπωση τιμολογίου που σχεδιάζει ο συνεργάτης για τον πελάτη πρέπει να τυπώνει, σε κάθε τιμολόγιο με απαλλαγή ΦΠΑ, τη διατύπωση της διάταξης που ισχύει σήμερα.
<!-- Customer need: a custom invoice layout must print the exemption reason with
the current (Law 5144/2024) wording, never a repealed article. -->
<!-- depends: ['l10n_gr_nt_aade_tables', 'account'] -->
<odoo>
<template id="acme_invoice_exemption_note" inherit_id="account.report_invoice_document">
<xpath expr="//div[hasclass('page')]" position="inside">
<t t-set="acme_reasons" t-value="o.invoice_line_ids.tax_ids.aade_vat_exemption_category_id"/>
<div t-if="acme_reasons" class="mt-3 small">
<strong>VAT exemption:</strong>
<ul class="list-unstyled mb-0">
<!-- l10n_gr_printed_reason() works on one record, hence the loop -->
<li t-foreach="acme_reasons" t-as="r">
<span t-out="r.code"/> - <span t-out="r.l10n_gr_printed_reason()"/>
</li>
</ul>
</div>
</xpath>
</template>
</odoo>aade.withheld.tax
model aade.withheld.tax (myDATA table 8.13): code Integer (required), name Char (required), value Float, fixed_amount Boolean; ordered by codeΠαρακρατούμενος φόρος (myDATA 8.13). Οι κωδικοί παρακρατούμενων φόρων της ΑΑΔΕ. Το value είναι ποσοστό· όταν είναι ενεργό το fixed_amount, είναι σταθερό ποσό. Διαβάστε τον όταν το module σας χρειάζεται τον επίσημο κωδικό ή συντελεστή μιας παρακράτησης.
Παράδειγμα
Το τμήμα αγορών θέλει να βλέπει σε κάθε παραστατικό ελεύθερου επαγγελματία το ποσό που θα παρακρατηθεί, με τον επίσημο κωδικό και συντελεστή παρακράτησης της ΑΑΔΕ.
# Customer need: purchasing wants freelancers' service bills to show the expected
# withholding before the accountant posts them (preview only; the withholding
# itself is applied by the suite's tax at payment).
# depends: ['l10n_gr_nt_aade_tables']
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
x_withheld_tax_id = fields.Many2one('aade.withheld.tax', string='Usual withholding')
class AccountMove(models.Model):
_inherit = 'account.move'
x_expected_withholding = fields.Monetary(compute='_compute_x_expected_withholding')
@api.depends('commercial_partner_id.x_withheld_tax_id', 'amount_untaxed', 'move_type', 'currency_id')
def _compute_x_expected_withholding(self):
for move in self:
wht = move.commercial_partner_id.x_withheld_tax_id
if not wht or move.move_type != 'in_invoice':
move.x_expected_withholding = 0.0
elif wht.fixed_amount: # value is an amount, not a rate
move.x_expected_withholding = wht.value
else:
move.x_expected_withholding = move.currency_id.round(move.amount_untaxed * wht.value / 100)account.tax.aade_vat_category_id
Many2one('aade.vat.category'), ondelete='restrict'Φόρος: κατηγορία ΦΠΑ myDATA. Η κατηγορία ΦΠΑ (πίνακας 8.2) του φόρου. Διαβάστε τη για να ταξινομήσετε γραμμές ανά κατηγορία ΦΠΑ. Μπορείτε να τη συμπληρώσετε σε φόρους που δημιουργεί το module σας· όταν η κατηγορία είναι ο κωδικός 7 (απαλλαγή), ένας constraint απαιτεί να συμπληρωθεί και το aade_vat_exemption_category_id, οπότε γράψτε τα δύο στο ίδιο write.
Παράδειγμα
Μετά από μετάπτωση, ο συνεργάτης θέλει έναν εβδομαδιαίο έλεγχο που επισημαίνει κάθε φόρο ΦΠΑ χωρίς κατηγορία ΦΠΑ myDATA, πριν φτάσουν στην προθεσμία υποβολής τα τιμολόγια που τον χρησιμοποιούν.
# Customer need: after a migration some VAT taxes were created by hand; every week
# before the filing deadline, flag VAT taxes still missing a myDATA VAT category.
# Scheduled action (ir.cron), Python code on model account.tax;
# module installed: l10n_gr_nt_aade_tables. Server-action code takes no imports.
missing = model.search([
('company_id', 'in', env.companies.ids),
('mydata_type', '=', '0'),
('type_tax_use', 'in', ('sale', 'purchase')),
('aade_vat_category_id', '=', False),
])
if missing:
used = env['account.move.line'].search_count(
[('tax_ids', 'in', missing.ids), ('parent_state', '=', 'posted')])
log('VAT taxes without myDATA VAT category: %s (on %s posted lines)'
% (', '.join(missing.mapped('name')), used), level='warning')
# Fix from the tax form; for category 7 (exempt) set aade_vat_exemption_category_id
# in the same write, or the suite's constraint rejects it.account.tax.aade_vat_exemption_category_id
Many2one('aade.vat.exemption.category'), ondelete='restrict'Φόρος: αιτία εξαίρεσης ΦΠΑ. Η αιτία εξαίρεσης (πίνακας 8.3) ενός φόρου 0% ή απαλλαγής. Απαιτείται όταν η κατηγορία ΦΠΑ είναι ο κωδικός 7. Διαβάστε την για να τυπώσετε την αιτία (μέσω της l10n_gr_printed_reason) ή για να ομαδοποιήσετε γραμμές ανά εξαίρεση.
Παράδειγμα
Ο λογιστής μιας εξαγωγικής επιχείρησης χρειάζεται κάθε τρίμηνο τα σύνολα των πωλήσεων με απαλλαγή ΦΠΑ ανά λόγο απαλλαγής, για να τα συμφωνήσει με τη δήλωση ΦΠΑ.
# Customer need: an exporter's accountant reconciles exempt sales per exemption
# reason with the VAT return each quarter.
# depends: ['l10n_gr_nt_aade_tables']
from odoo import models
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
def acme_exempt_sales_by_reason(self, date_from, date_to):
lines = self.search([
('parent_state', '=', 'posted'), ('company_id', '=', self.env.company.id),
('move_id.move_type', 'in', ('out_invoice', 'out_refund')),
('date', '>=', date_from), ('date', '<=', date_to),
('tax_ids.aade_vat_exemption_category_id', '!=', False)])
totals = {}
for line in lines:
for reason in line.tax_ids.aade_vat_exemption_category_id:
totals[reason] = totals.get(reason, 0.0) - line.balance
return [(r.code, r.l10n_gr_printed_reason(), amount)
for r, amount in sorted(totals.items(), key=lambda kv: kv[0].code)]account.tax.mydata_type
Selection [('0','VAT'),('1','Withholding Tax'),('2','Fees'),('3','Other Taxes'),('4','Stamp Duty'),('5','Deductions'),('6','Other')], required, default '0'Φόρος: είδος φόρου myDATA. Η οικογένεια φόρων του myDATA στην οποία ανήκει ο φόρος. Διαβάστε το για να ξεχωρίζετε στις αναφορές σας τον ΦΠΑ από τις παρακρατήσεις, τα τέλη, το χαρτόσημο και τους λοιπούς φόρους. Όταν το module σας δημιουργεί φόρο που δεν είναι ΦΠΑ, ορίστε τη σωστή τιμή· η προεπιλογή είναι ΦΠΑ.
Παράδειγμα
Το τμήμα εισπράξεων χρειάζεται λίστα με τα καταχωρημένα τιμολόγια που φέρουν φόρο παρακράτησης, ώστε να ζητά από κάθε πελάτη τη βεβαίωση παρακράτησης.
# Customer need: collections must chase the client's withholding certificate for
# every invoice that carries a withholding tax; give them a filter for it.
# depends: ['l10n_gr_nt_aade_tables']
from odoo import api, fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
x_has_withholding = fields.Boolean(
compute='_compute_x_has_withholding', store=True, string='Has withholding')
@api.depends('invoice_line_ids.tax_ids.mydata_type')
def _compute_x_has_withholding(self):
for move in self:
# '1' = Withholding Tax in the myDATA tax family
move.x_has_withholding = any(
t.mydata_type == '1' for t in move.invoice_line_ids.tax_ids)
# In a search view: <filter name="withholding" string="With withholding"
# domain="[('x_has_withholding', '=', True), ('state', '=', 'posted')]"/>Τιμολόγηση προς το Δημόσιο
depends: ['l10n_gr_nt_b2g']
Εκδίδει το ηλεκτρονικό τιμολόγιο προς φορείς του Δημοσίου με τα στοιχεία που απαιτεί το δίκτυο Peppol και παρακολουθεί την απάντηση του φορέα.
account.move.b2g
Boolean, compute='_compute_b2g', store=True, readonly=False, copy=FalseΤιμολόγιο: προς φορέα του Δημοσίου. Αληθές όταν ο πελάτης (ο commercial partner του) έχει κωδικό αναθέτουσας αρχής και η κίνηση είναι τιμολόγιο ή πιστωτικό πώλησης (όχι απόδειξη). Διαβάστε το όταν η λογική ή τα views σας πρέπει να χειρίζονται διαφορετικά τα τιμολόγια B2G. Είναι επεξεργάσιμο, αλλά ξαναϋπολογίζεται όταν αλλάζει ο πελάτης.
Παράδειγμα
Ένας προμηθευτής νοσοκομείων και υπουργείων θέλει κάθε τιμολόγιο προς το Δημόσιο να δημιουργεί υπενθύμιση είσπραξης για το αρμόδιο τμήμα, επειδή οι φορείς πληρώνουν με καθυστέρηση.
# Customer need: public bodies pay late; every posted B2G invoice gets a
# follow-up activity for the collections team 60 days after the invoice date.
# depends: ['l10n_gr_nt_b2g']
from datetime import timedelta
from odoo import _, models
class AccountMove(models.Model):
_inherit = 'account.move'
def _post(self, soft=True):
posted = super()._post(soft=soft)
for move in posted.filtered('b2g'):
move.activity_schedule(
'mail.mail_activity_data_todo',
date_deadline=(move.invoice_date or move.date) + timedelta(days=60),
summary=_('Check payment by the public body'),
user_id=move.invoice_user_id.id or self.env.uid)
return postedaccount.move.b2g_buyer_reference
Char, copy=FalseΤιμολόγιο: αναφορά αγοραστή. Η αναφορά που έδωσε ο φορέας για τη συγκεκριμένη αγορά (π.χ. η υπηρεσία του). Απαιτείται πριν καταχωρηθεί τιμολόγιο B2G. Μεταφέρετέ την από την παραγγελία ή ορίστε την στον δικό σας wizard.
Παράδειγμα
Ένας προμηθευτής νοσοκομείων πρέπει να καταγράφει σε κάθε παραγγελία την αναφορά αγοραστή που δίνει το νοσοκομείο, ώστε η καταχώρηση του τιμολογίου να μη σταματά σε αυτόν τον έλεγχο.
# Customer need: hospitals give a department/requisition reference with each
# order; it must reach the invoice, which cannot be posted without it.
# depends: ['l10n_gr_nt_b2g', 'sale']
from odoo import _, models
from odoo.exceptions import UserError
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
for order in self:
if (order.partner_id.commercial_partner_id.b2g_party_identification
and not order.client_order_ref):
raise UserError(_('%s is for a public body: fill in the customer reference.', order.name))
return super().action_confirm()
def _prepare_invoice(self):
vals = super()._prepare_invoice()
if self.client_order_ref:
vals['b2g_buyer_reference'] = self.client_order_ref
return valsaccount.move.b2g_contract_document_reference
Char, copy=FalseΤιμολόγιο: αναγνωριστικό σύμβασης (ΑΔΑΜ). Ο ΑΔΑΜ της σύμβασης ή 0 όταν δεν υπάρχει. Απαιτείται πριν καταχωρηθεί τιμολόγιο B2G. Συμπληρώστε τον από τα δικά σας στοιχεία σύμβασης ή παραγγελίας πριν την καταχώρηση.
Παράδειγμα
Μια επιχείρηση που κερδίζει δημόσιους διαγωνισμούς θέλει ο ΑΔΑΜ της σύμβασης να καταχωρίζεται μία φορά στην παραγγελία και να μεταφέρεται σε κάθε τιμολόγιο που εκδίδεται βάσει αυτής.
# Customer need: tenders produce a contract with an ΑΔΑΜ; sales enter it once on
# the order and every invoice of the order carries it (0 when there is none).
# depends: ['l10n_gr_nt_b2g', 'sale']
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
x_adam = fields.Char(
string='Contract ΑΔΑΜ', copy=False,
help='Leave empty when the sale has no contract; the invoice then gets 0.')
def _prepare_invoice(self):
vals = super()._prepare_invoice()
vals['b2g_contract_document_reference'] = (self.x_adam or '0').strip().upper()
return valsaccount.move.b2g_party_identification
Char, compute='_compute_b2g', store=True, readonly=False, copy=FalseΤιμολόγιο: κωδικός αναθέτουσας αρχής. Παίρνει αρχικά τον κωδικό του πελάτη και μπορεί να διαφέρει ανά τιμολόγιο, γιατί ένας ΑΦΜ μπορεί να έχει περισσότερες αναθέτουσες αρχές. Γράψτε τον πριν την καταχώρηση όταν το module σας γνωρίζει τη σωστή αρχή για τη συγκεκριμένη πώληση. Είναι ένα από τα fields που ελέγχει η σουίτα πριν καταχωρηθεί τιμολόγιο B2G.
Παράδειγμα
Ένας προμηθευτής υπουργείου με πολλές αναθέτουσες αρχές κάτω από τον ίδιο ΑΦΜ θέλει κάθε τιμολόγιο να φέρει τον κωδικό της αρχής που αντιστοιχεί στη διεύθυνση παράδοσης.
# Customer need: one ministry (one tax number) has several contracting
# authorities; each delivery address carries its own code, and the invoice must
# use the code of the address the goods went to.
# depends: ['l10n_gr_nt_b2g', 'sale']
from odoo import models
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _prepare_invoice(self):
vals = super()._prepare_invoice()
# the company partner keeps its own code too: that is what marks the move as B2G
code = self.partner_shipping_id.b2g_party_identification
if code:
vals['b2g_party_identification'] = code
return valsres.partner.b2g_party_identification
Char; constraint: Latin capitals, digits and dots only (e.g. 1032.E00400.0001)Πελάτης: κωδικός αναθέτουσας αρχής. Ο κωδικός αναθέτουσας αρχής που δίνει ο φορέας. Όταν είναι συμπληρωμένος στον πελάτη, τα τιμολόγιά του χαρακτηρίζονται B2G. Μπορείτε να τον γράψετε από δική σας εισαγωγή ή συγχρονισμό· ένας constraint απορρίπτει τιμή με ελληνικούς ή άλλους μη επιτρεπτούς χαρακτήρες, οπότε μετατρέψτε την πρώτα σε λατινικά κεφαλαία. Απαιτεί το l10n_gr_nt_b2g (που εξαρτάται από το l10n_gr_nt_mydata_invoice).
Παράδειγμα
Ο συνεργάτης που εισάγει λίστα πελατών του Δημοσίου πρέπει πρώτα να καθαρίσει τους κωδικούς αναθέτουσας αρχής, γιατί συχνά περιέχουν ελληνικά γράμματα που μοιάζουν με λατινικά και το πεδίο δεν τους δέχεται.
# Customer need: importing public-body customers from a tender list where codes
# were typed with Greek letters that look Latin (Ε, Ο, Α ...); normalise first,
# because the field accepts only Latin capitals, digits and dots.
# Run in odoo-bin shell from the console (env is given); module installed: l10n_gr_nt_b2g
import re
GREEK_TO_LATIN = str.maketrans('ΑΒΕΖΗΙΚΜΝΟΡΤΥΧ', 'ABEZHIKMNOPTYX')
rows = [ # (tax number, authority code) as received
('094000001', '1032.Ε00400.0001'),
('094000002', '1045.e00210.0003'),
]
for vat, raw in rows:
code = raw.strip().upper().translate(GREEK_TO_LATIN)
if not re.fullmatch(r'[A-Z0-9.]+', code):
print('Skipped, still invalid:', raw)
continue
partner = env['res.partner'].search(
[('vat', 'in', (vat, 'EL' + vat)), ('is_company', '=', True)], limit=1)
if partner:
partner.b2g_party_identification = code
else:
print('No customer with tax number', vat)
env.cr.commit()Λογιστικό Σχέδιο ΕΛΠ: Επέκταση
depends: ['l10n_gr_nt_chart_ext']
Προσθέτει σε κάθε λογαριασμό, φόρο και ημερολόγιο τα στοιχεία που απαιτεί ο χαρακτηρισμός συναλλαγών, και δημιουργεί τις σειρές παραστατικών και τους φόρους παρακράτησης, χαρτοσήμου και τελών.
account.journal.l10n_gr_invoice_type_id
Many2one('aade.invoice.type')Ημερολόγιο: τύπος παραστατικού ΑΑΔΕ. Ο τύπος παραστατικού (πίνακας 8.1) που εκδίδει το ημερολόγιο. Από αυτόν εξαρτάται και η αρίθμηση της σειράς, που πρέπει να είναι συνεχής ανά τύπο, γι' αυτό είναι ρύθμιση της φόρμας του ημερολογίου: διαβάστε τον στον κώδικά σας για να βρείτε το ημερολόγιο ενός τύπου παραστατικού, αλλά μην τον αλλάζετε από κώδικα σε ημερολόγιο που έχει ήδη καταχωρημένα παραστατικά.
Παράδειγμα
Μια επιχείρηση που πουλά και αγαθά και υπηρεσίες θέλει οι παραγγελίες που περιέχουν μόνο υπηρεσίες να τιμολογούνται αυτόματα από τη σειρά του Τιμολογίου Παροχής Υπηρεσιών.
# Customer need: a company that sells goods and services must issue service-only
# orders as Service Invoices (2.1) from that series, not the goods series.
# depends: ['l10n_gr_nt_chart_ext', 'sale']
from odoo import models
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _prepare_invoice(self):
vals = super()._prepare_invoice()
lines = self.order_line.filtered(lambda l: not l.display_type)
if lines and all(l.product_id.type == 'service' for l in lines):
# read only: the document type is set on the journal form, never from code
journal = self.env['account.journal'].search([
('company_id', '=', self.company_id.id), ('type', '=', 'sale'),
('l10n_gr_invoice_type_id.code', '=', '2.1')], limit=1)
if journal:
vals['journal_id'] = journal.id
return valsaccount.move.l10n_gr_fees_total
Monetary(currency_field='currency_id'), compute='_compute_l10n_gr_fees_total', store=TrueΤιμολόγιο: σύνολο τελών. Το άθροισμα των επιβαρύνσεων στις γραμμές του τιμολογίου. Είναι αποθηκευμένο, οπότε μπορείτε να κάνετε αναζήτηση, ομαδοποίηση και αναφορές με αυτό. Μόνο για ανάγνωση: οι επιβαρύνσεις αλλάζουν μέσω των ειδών, όχι από αυτό το field.
Παράδειγμα
Ένας διανομέας ποτών χρειάζεται τα τέλη ανακύκλωσης που εισέπραξε κάθε μήνα, μετά την αφαίρεση των πιστωτικών, για να υποβάλει τη μηνιαία δήλωση τελών.
# Customer need: a beverage distributor declares the recycling fees it collected
# each month; the accountant wants the monthly totals, credit notes deducted.
# depends: ['l10n_gr_nt_chart_ext']
from odoo import models
class AccountMove(models.Model):
_inherit = 'account.move'
def acme_fees_by_month(self, year):
domain = [
('company_id', '=', self.env.company.id), ('state', '=', 'posted'),
('move_type', 'in', ('out_invoice', 'out_refund')),
('invoice_date', '>=', f'{year}-01-01'), ('invoice_date', '<=', f'{year}-12-31'),
('l10n_gr_fees_total', '!=', 0)]
result = {}
for month, move_type, total in self._read_group(
domain, ['invoice_date:month', 'move_type'], ['l10n_gr_fees_total:sum']):
sign = -1 if move_type == 'out_refund' else 1
result[month] = result.get(month, 0.0) + sign * total
return resultaccount.tax.l10n_gr_deductible
Boolean, default TrueΦόρος: ο ΦΠΑ εκπίπτει. Δείχνει αν ο ΦΠΑ του φόρου εκπίπτει κατά το άρθρο 30 του Κώδικα ΦΠΑ. Όταν είναι ανενεργό, ο φόρος προστίθεται στο κόστος (λογαριασμός μη εκπιπτόμενου ΦΠΑ για δαπάνες, αξία κτήσης για πάγια) και η γραμμή χαρακτηρίζεται category2_5. Δεν αφορά την έκπτωση της δαπάνης από το εισόδημα. Διαβάστε το στη δική σας λογική αγορών· ορίστε το σε φόρους αγορών μη εκπιπτόμενου ΦΠΑ που δημιουργεί το module σας (επιβατικά αυτοκίνητα, φιλοξενία).
Παράδειγμα
Μια εταιρεία με στόλο επιβατικών αυτοκινήτων θέλει να αποτρέπεται η καταχώριση τιμολογίων καυσίμων ή μισθώσεων με εκπιπτόμενο ΦΠΑ.
# Customer need: fuel and rentals for passenger cars carry non-deductible VAT;
# stop a vendor bill from being posted with deductible VAT on such costs.
# depends: ['l10n_gr_nt_chart_ext']
from odoo import _, fields, models
from odoo.exceptions import UserError
class ProductCategory(models.Model):
_inherit = 'product.category'
x_passenger_car_cost = fields.Boolean(string='Passenger car cost')
class AccountMove(models.Model):
_inherit = 'account.move'
def action_post(self):
for move in self.filtered(lambda m: m.move_type in ('in_invoice', 'in_refund')):
wrong = move.invoice_line_ids.filtered(
lambda l: l.product_id.categ_id.x_passenger_car_cost
and l.tax_ids.filtered(lambda t: t.mydata_type == '0' and t.l10n_gr_deductible))
if wrong:
raise UserError(_('%(bill)s: use the non-deductible VAT on %(lines)s.',
bill=move.display_name,
lines=', '.join(wrong.mapped('product_id.display_name'))))
return super().action_post()account.tax.l10n_gr_tax_code
Char, readonly, copy=False (e.g. 'VAT-S-24')Φόρος: κωδικός τοπικοποίησης. Ο κωδικός του φόρου στον πίνακα φόρων της τοπικοποίησης, π.χ. VAT-S-24. Δεν εμφανίζεται στον πελάτη. Χρησιμοποιήστε τον για να εντοπίζετε με ασφάλεια έναν φόρο από κώδικα, αντί για το όνομα, που μπορεί να το αλλάξει ο χρήστης. Μόνο για ανάγνωση.
Παράδειγμα
Κατά τη μετάπτωση των ειδών ο συνεργάτης πρέπει να αντιστοιχίσει κάθε παλαιό κωδικό ΦΠΑ στον σωστό φόρο πωλήσεων και αγορών, ακόμη κι αν οι χρήστες έχουν μετονομάσει τους φόρους.
# Customer need: migrating products from a legacy ERP whose VAT codes are 1/2/3;
# map them to taxes by localization code, not by name (users rename taxes).
# Run in odoo-bin shell from the console (env is given); module installed: l10n_gr_nt_chart_ext
LEGACY_TO_CODES = { # legacy VAT code -> (sale tax code, purchase tax code)
'1': ('VAT-S-24', 'VAT-P-24'),
'2': ('VAT-S-13', 'VAT-P-13'),
'3': ('VAT-S-6', 'VAT-P-6'),
}
Tax = env['account.tax']
company_domain = Tax._check_company_domain(env.company)
products = [('ACME-001', '1'), ('ACME-002', '2')] # (internal reference, legacy VAT code)
for ref, legacy in products:
sale_code, purchase_code = LEGACY_TO_CODES[legacy]
sale_tax = Tax.search([*company_domain, ('l10n_gr_tax_code', '=', sale_code)], limit=1)
purchase_tax = Tax.search([*company_domain, ('l10n_gr_tax_code', '=', purchase_code)], limit=1)
product = env['product.template'].search([('default_code', '=', ref)], limit=1)
if product and sale_tax and purchase_tax:
product.write({'taxes_id': [(6, 0, sale_tax.ids)],
'supplier_taxes_id': [(6, 0, purchase_tax.ids)]})
else:
print('Not mapped:', ref, legacy)
env.cr.commit()l10n_gr.fee
model l10n_gr.fee (per-unit charge, AADE 8.7/8.5/8.6): name Char (required), sequence, active, company_id (required), l10n_gr_tax_type Selection [fees_category, other_tax, stamp_duty] (required, default fees_category), fees_category_id / other_tax_id / stamp_duty_id, per_unit Boolean (default True), amount Float (required, > 0), legal_basis Char, account_id, l10n_gr_in_vat_base Boolean (default True), l10n_gr_receipt_label Char, l10n_gr_verified Boolean, tax_id (readonly, managed), l10n_gr_tax_group_id (readonly, managed); ordered by sequence, idΕπιβάρυνση ανά τεμάχιο. Επιβάρυνση που εισπράττεται μαζί με ένα είδος, π.χ. το τέλος ανακύκλωσης στο πλαστικό μπουκάλι ή η εισφορά προστασίας περιβάλλοντος στη σακούλα. Κάθε επιβάρυνση ανήκει σε μία εταιρεία και ορίζει τον κωδικό ΑΑΔΕ (πίνακας 8.7, 8.5 ή 8.6), το ποσό ανά τεμάχιο (ή, όταν το per_unit είναι ανενεργό, το ποσοστό επί της καθαρής αξίας της γραμμής), τον λογαριασμό που πιστώνεται και αν η επιβάρυνση μπαίνει στη βάση του ΦΠΑ. Το ποσό πρέπει να είναι μεγαλύτερο από μηδέν. Η σουίτα δημιουργεί και συντηρεί τον δικό της φόρο και την ομάδα φόρων, οπότε μην αλλάζετε τα tax_id και l10n_gr_tax_group_id. Το module σας μπορεί να διαβάζει επιβαρύνσεις ή να δημιουργεί νέα για μια εταιρεία και να τη συνδέει με είδη μέσω του product.template.l10n_gr_fee_id.
Παράδειγμα
Πριν από την έναρξη λειτουργίας, ο συνεργάτης θέλει μια λίστα με τα τέλη ανά τεμάχιο που δεν έχει ελέγξει ακόμη ο λογιστής, μαζί με το πλήθος των ειδών που χρησιμοποιούν το καθένα.
# Customer need: before go-live the accountant confirms every per-unit fee
# (amount, AADE code, account); list the fees nobody has verified yet.
# Scheduled action (ir.cron), Python code on model l10n_gr.fee;
# module installed: l10n_gr_nt_chart_ext. Server-action code takes no imports.
unverified = model.search([
('company_id', 'in', env.companies.ids),
('l10n_gr_verified', '=', False),
])
for fee in unverified:
used_by = env['product.template'].search_count([('l10n_gr_fee_id', '=', fee.id)])
log('Fee "%s" (%s %s, %s) not verified, used by %s products' % (
fee.name, fee.amount, 'per unit' if fee.per_unit else '% of net value',
fee.company_id.name, used_by), level='warning')
# Read only here: the suite manages tax_id and l10n_gr_tax_group_id of each fee.product.template.l10n_gr_fee_id
Many2one('l10n_gr.fee'), domain: company_id = current companyΕίδος: επιβάρυνση ανά τεμάχιο. Η επιβάρυνση που χρεώνεται μαζί με το είδος (μία ανά είδος). Υπολογίζεται στη γραμμή του τιμολογίου και αθροίζεται στο τιμολόγιο. Ορίστε τη από τη δική σας εισαγωγή ή τον συγχρονισμό ειδών· η επιβάρυνση πρέπει να ανήκει στην εταιρεία του είδους.
Παράδειγμα
Ένας διανομέας ποτών που εισάγει τον κατάλογο του προμηθευτή του θέλει κάθε είδος σε πλαστική φιάλη να παίρνει αυτόματα το τέλος ανακύκλωσης.
# Customer need: the supplier catalogue marks which items are plastic bottles;
# those products must carry the company's recycling fee from the first sale.
# Run in odoo-bin shell from the console (env is given); module installed: l10n_gr_nt_chart_ext
company = env.company
fee = env['l10n_gr.fee'].search([
('company_id', '=', company.id), ('l10n_gr_tax_type', '=', 'fees_category'),
('name', 'ilike', 'recycl')], limit=1)
assert fee, 'Create the recycling fee for %s first' % company.name
catalogue = [('BTL-500', True), ('BTL-1500', True), ('CAN-330', False)] # (reference, plastic bottle)
refs = [ref for ref, plastic in catalogue if plastic]
# the fee belongs to one company: link it only to that company's (or shared) products
products = env['product.template'].search([
('default_code', 'in', refs), ('company_id', 'in', (company.id, False))])
products.write({'l10n_gr_fee_id': fee.id})
print('%s products now carry %s' % (len(products), fee.name))
env.cr.commit()product.template.l10n_gr_income_category_id
Many2one('aade.income.category')Είδος: ειδική κατηγορία εσόδων ΑΑΔΕ. Συμπληρώνεται μόνο όταν το είδος ανήκει σε άλλη κατηγορία εσόδων από εκείνη του λογαριασμού εσόδων του. Όταν μένει κενό, ισχύει η κατηγορία του λογαριασμού, και αυτό είναι το σωστό για τα περισσότερα είδη. Ορίστε το από τα δικά σας δεδομένα ειδών όταν ένα είδος χρειάζεται διαφορετικό χαρακτηρισμό.
Παράδειγμα
Ένα κατάστημα που πουλά και υπηρεσίες εγκατάστασης θέλει οι υπηρεσίες αυτές να χαρακτηρίζονται ως έσοδο από παροχή υπηρεσιών στο myDATA, παρότι χρησιμοποιούν τον ίδιο λογαριασμό εσόδων με τα αγαθά.
# Customer need: installation services post to the same income account as the
# goods, but myDATA wants them as services income (category1_3); override only them.
# Run in odoo-bin shell from the console (env is given); module installed: l10n_gr_nt_chart_ext
services = env['aade.income.category'].search([('code', '=', 'category1_3')], limit=1)
assert services, 'category1_3 not found'
products = env['product.template'].search([
('type', '=', 'service'),
('categ_id.name', '=', 'Installation'),
('l10n_gr_income_category_id', '=', False), # keep any override already set
])
products.write({'l10n_gr_income_category_id': services.id})
print('%s installation services now classified as %s' % (len(products), services.display_name))
# Empty override = the income account's classification, right for every other product.
env.cr.commit()res.partner.l10n_gr_reduced_vat_status
Selection ['normal', 'taxable', 'legal_person'] (labels: normal regime; reduced, taxable person established on the island; reduced, non-taxable legal person), required, default 'normal'; commercial fieldΣυναλλασσόμενος: καθεστώς μειωμένου ΦΠΑ νησιών. Δείχνει αν στον συναλλασσόμενο μπορεί να εφαρμοστεί ο μειωμένος συντελεστής ΦΠΑ νησιών. Ο τόπος μόνος του δεν αρκεί: ιδιώτης που κατοικεί στο νησί μένει στον κανονικό συντελεστή. Είναι commercial field: ορίστε το στην εταιρεία και περνά αυτόματα στις διευθύνσεις της. Ορίστε το από τη δική σας καταχώρηση ή εισαγωγή· απαιτεί το l10n_gr_nt_chart_ext.
Παράδειγμα
Ένας χονδρέμπορος σε νησί, όταν μεταφέρει τους πελάτες του στο Odoo, θέλει να ορίσει το καθεστώς μειωμένου ΦΠΑ νησιών μόνο στους πελάτες που το δικαιούνται.
# Customer need: a wholesaler on Rhodes migrates customers from its old ERP,
# which flagged island businesses; carry the flag so the reduced rate applies
# only to those customers.
# Run in odoo-bin shell from the console (env is given); module installed: l10n_gr_nt_chart_ext
LEGACY_STATUS = {'N': 'normal', 'I': 'taxable', 'L': 'legal_person'}
rows = [('094000001', 'I'), ('094000002', 'N'), ('094000003', 'L')] # (tax number, legacy flag)
Partner = env['res.partner']
for vat, flag in rows:
partner = Partner.search([('vat', 'in', (vat, 'EL' + vat))], limit=1).commercial_partner_id
if not partner:
print('Not found:', vat)
continue
# commercial field: set it on the company, Odoo copies it to its addresses
partner.l10n_gr_reduced_vat_status = LEGACY_STATUS[flag]
env.cr.commit()res.partner.l10n_gr_vat45_buyer
BooleanΣυναλλασσόμενος: υποκείμενος αγοραστής άρθρου 45. Σημειώνει αγοραστή υποκείμενο στον ΦΠΑ με ελληνικό ΑΦΜ που αγοράζει είδη του άρθρου 45 του Κώδικα ΦΠΑ. Στις γραμμές πώλησης ειδών του άρθρου 45 προς τέτοιον αγοραστή η σουίτα αντικαθιστά τον ΦΠΑ με τον φόρο αντιστροφής· τα υπόλοιπα είδη κρατούν τον φόρο τους. Ορίστε το στον commercial partner από τα δικά σας δεδομένα· διαβάστε το αν το module σας υπολογίζει μόνο του τιμές ή φόρους.
Παράδειγμα
Ένας πωλητής αγαθών του άρθρου 45 θέλει οι προσφορές προς τους αγοραστές που εμπίπτουν στη διάταξη να αναφέρουν εξαρχής ότι για τα αγαθά αυτά ισχύει αντίστροφη επιβάρυνση ΦΠΑ.
<!-- Customer need: quotations to Article 45 buyers must tell the buyer that the
VAT of Article 45 goods is reverse charged, before the invoice arrives. -->
<!-- depends: ['l10n_gr_nt_chart_ext', 'sale'] -->
<odoo>
<template id="acme_quote_art45_note" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[hasclass('page')]" position="inside">
<p t-if="doc.partner_id.commercial_partner_id.l10n_gr_vat45_buyer" class="mt-3 small">
Article 45 goods on this quotation are invoiced with reverse charge of VAT;
the other goods keep their VAT.
</p>
</xpath>
</template>
</odoo>Εισαγωγή λογιστικού σχεδίου
depends: ['l10n_gr_nt_chart_import']
Εισάγει το λογιστικό σχέδιο του πελάτη από Excel και δίνει σε κάθε λογαριασμό τον αντίστοιχο λογαριασμό του προτύπου, χαρακτηρισμό και φόρους.
account.account.l10n_gr_legacy_code
Char, index='btree_not_null', copy=FalseΛογαριασμός: κωδικός προηγούμενου συστήματος. Ο κωδικός που είχε ο λογαριασμός στο σχέδιο από το οποίο εισήχθη (π.χ. ο παλιός κωδικός ΕΓΛΣ όταν η εταιρεία περνά σε ΕΛΠ). Το λογιστήριο εξακολουθεί να αναγνωρίζει τον λογαριασμό με αυτόν τον κωδικό. Χρησιμοποιήστε τον στο δικό σας module μετάπτωσης ή εισαγωγής, για να αντιστοιχίσετε δεδομένα από το προηγούμενο σύστημα του πελάτη στους νέους λογαριασμούς. Έχει index για αναζήτηση. Συμπληρώνεται από την εισαγωγή· απαιτεί το l10n_gr_nt_chart_import.
Παράδειγμα
Μια εταιρεία που περνά στο νέο λογιστικό σχέδιο πρέπει να καταχωρίσει στους αντίστοιχους νέους λογαριασμούς τα υπόλοιπα έναρξης, που εξάγονται από το παλαιό ERP με τους παλαιούς κωδικούς λογαριασμών.
# Customer need: opening balances come from the old ERP keyed by the old ΕΓΛΣ
# codes; book them as one draft opening entry on the new accounts.
# Run in odoo-bin shell from the console (env is given); module installed: l10n_gr_nt_chart_import
from odoo import Command, fields
balances = [('38.00.00', 1520.40), ('38.03.00', 820.40), ('40.00.00', -2340.80)] # (old code, debit - credit)
Account = env['account.account']
lines, missing = [], []
for old_code, amount in balances:
acc = Account.search([('company_ids', 'in', env.company.id),
('l10n_gr_legacy_code', '=', old_code)], limit=1)
if not acc:
missing.append(old_code)
continue
lines.append(Command.create({'account_id': acc.id, 'name': 'Opening balance',
'debit': max(amount, 0.0), 'credit': max(-amount, 0.0)}))
assert not missing, 'No account for old codes: %s' % missing
journal = env['account.journal'].search([('type', '=', 'general'), ('company_id', '=', env.company.id)], limit=1)
move = env['account.move'].create({
'move_type': 'entry', 'journal_id': journal.id, 'date': fields.Date.to_date('2027-01-01'),
'ref': 'Opening balances from legacy ERP', 'line_ids': lines})
print('Draft opening entry', move.id, '- the accountant reviews and posts it')
env.cr.commit()Ανακεφαλαιωτικοί Πίνακες Ενδοκοινοτικών (Φ4 και Φ5)
depends: ['l10n_gr_nt_ec_purchase']
Συντάσσει τους ανακεφαλαιωτικούς πίνακες των ενδοκοινοτικών παραδόσεων και αποκτήσεων από τις κινήσεις της περιόδου, με τον διορθωτικό πίνακα και το αρχείο του TAXISnet.
account.move.l10n_gr_triangular
Boolean, copy=False; constraint on postingΤιμολόγιο: τριγωνική συναλλαγή. Σημειώνει ότι η εταιρεία συμμετέχει σε τριγωνική συναλλαγή (άρθρο 15 §2 του Κώδικα ΦΠΑ)· η αξία τότε πηγαίνει στη στήλη 6 του ανακεφαλαιωτικού πίνακα αντί για τη στήλη 5. Ορίστε το από τη δική σας λογική, π.χ. από ένδειξη στην παραγγελία, πριν την καταχώρηση. Κατά την καταχώρηση, ένας constraint απαιτεί τουλάχιστον μία γραμμή με σήμανση ενδοκοινοτικού φόρου, εκτός αν το παραστατικό εξαιρείται από τον πίνακα· γι' αυτό ορίστε και τον ενδοκοινοτικό φόρο στις γραμμές.
Παράδειγμα
Ο προμηθευτής ενός εμπόρου στην ΕΕ στέλνει τα αγαθά απευθείας στον πελάτη του, επίσης στην ΕΕ. Ο έμπορος θέλει οι πωλήσεις αυτές να σημειώνονται ως τριγωνικές, ώστε να καταχωρίζονται στη σωστή στήλη του ανακεφαλαιωτικού πίνακα.
# Customer need: a Greek trader buys from an Italian supplier and the goods ship
# straight to a German customer; those invoices go to column 6 of the VIES statement.
# depends: ['l10n_gr_nt_ec_purchase', 'sale']
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
x_triangular = fields.Boolean(
string='Triangular (shipped from another EU state)', copy=False,
help="Set when the goods travel from the supplier's EU state straight to the customer.")
def _prepare_invoice(self):
vals = super()._prepare_invoice()
eu = self.env.ref('base.europe').country_ids
country = self.partner_id.commercial_partner_id.country_id
# posting also needs an intra-EU tax on the lines: use the EU fiscal position
vals['l10n_gr_triangular'] = bool(
self.x_triangular and country in eu and country.code != 'GR')
return valsΈλεγχος Τύπου Λήπτη
depends: ['l10n_gr_nt_guard_partner_rule']
Περιορίζει κάθε ημερολόγιο στον τύπο λήπτη που του αντιστοιχεί, ώστε ένα τιμολόγιο χονδρικής να μην εκδοθεί σε ιδιώτη.
res.company.l10n_gr_vat_exempt_small_entity
BooleanΕταιρεία: απαλλασσόμενη από ΦΠΑ μικρή επιχείρηση. Δηλώνει ότι η εταιρεία είναι απαλλασσόμενη από ΦΠΑ μικρή επιχείρηση. Όταν είναι ενεργό, το ανώτατο ποσό του απλοποιημένου τιμολογίου δεν εφαρμόζεται, γιατί το άρθρο 10 §1 του ν. 4308/2014 το επιτρέπει χωρίς όριο σε αυτή την περίπτωση. Η τιμή δηλώνεται και δεν υπολογίζεται, γιατί το όριο κύκλου εργασιών αλλάζει με εγκύκλιο και η ένταξη στο καθεστώς είναι επιλογή της επιχείρησης. Είναι field της εταιρείας που εμφανίζεται και ορίζεται στη φόρμα του ημερολογίου· διαβάστε το στους δικούς σας ελέγχους που εξαρτώνται από το καθεστώς.
Παράδειγμα
Μια μικρή επιχείρηση που υπάγεται στο καθεστώς απαλλαγής ΦΠΑ δεν πρέπει να επιβεβαιώνει παραγγελία στην οποία κάποια γραμμή εξακολουθεί να χρεώνει ΦΠΑ.
# Customer need: a VAT-exempt small business must never charge VAT; stop an
# order that still carries a VAT tax on any line.
# depends: ['l10n_gr_nt_guard_partner_rule', 'sale']
from odoo import _, models
from odoo.exceptions import UserError
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
for order in self.filtered(lambda o: o.company_id.l10n_gr_vat_exempt_small_entity):
taxed = order.order_line.filtered(
lambda l: l.tax_ids.filtered(lambda t: t.mydata_type == '0' and t.amount))
if taxed:
raise UserError(_('%(company)s is a VAT-exempt small business: remove VAT from %(lines)s.',
company=order.company_id.name,
lines=', '.join(taxed.mapped('product_id.display_name'))))
return super().action_confirm()Ψηφιακή διακίνηση
depends: ['l10n_gr_nt_movement']
Παρακολουθεί το δελτίο αποστολής μιας μεταφοράς ή ενός τιμολογίου σε όλο τον κύκλο ζωής του στην ΑΑΔΕ (καταχώρηση, σε διακίνηση, παράδοση, επιστροφή, ολοκλήρωση), μαζί με οχήματα, δρομολόγια διανομής και πώλησης επί αυτοκινήτου και την αντιστοίχιση ειδών στην πλευρά του λήπτη. Απαιτεί το l10n_gr_nt_movement, που εγκαθιστά μαζί του τα l10n_gr_nt_stock, stock_barcode, sms και το module των ληφθέντων παραστατικών.
account.move.l10n_gr_movement_id
fields.Many2one('l10n_gr.movement', compute='_compute_l10n_gr_movement', string='Digital Movement') (not stored)Διακίνηση του τιμολογίου. Η ψηφιακή διακίνηση του τιμολογίου, όταν αυτό επέχει θέση δελτίου αποστολής· αλλιώς κενό. Είναι computed και όχι stored: διαβάστε το σε εγγραφή, μην κάνετε αναζήτηση με αυτό.
Παράδειγμα
Το τιμολόγιο που επέχει θέση δελτίου αποστολής τυπώνει τον αριθμό του δελτίου, το όχημα και την τρέχουσα κατάσταση της διακίνησης.
<!-- depends: ['l10n_gr_nt_movement'] -->
<!-- Customer need: the printed invoice that is also the delivery note shows the note, the truck and where the goods are -->
<template id="report_invoice_acme_movement" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='informations']" position="inside">
<t t-set="movement" t-value="o.l10n_gr_movement_id"/>
<div class="col" t-if="movement" name="acme_movement">
<strong>Delivery note</strong>
<div t-field="movement.name"/>
<div t-if="movement.current_vehicle_id">
Vehicle: <span t-field="movement.current_vehicle_id.license_plate"/>
</div>
<div t-if="movement.status_id">
Status: <span t-field="movement.status_id"/>
</div>
</div>
</xpath>
</template>l10n_gr.movement
_name = 'l10n_gr.movement' (Digital Movement); _inherit = ['mail.thread', 'mail.activity.mixin']; main fields: name (delivery note), role ('issuer' | 'recipient'), company_id, picking_id, move_id, status_id, event_idsΨηφιακή διακίνηση. Μία εγγραφή ανά δελτίο αποστολής, που τη δημιουργεί η σουίτα όταν εκδίδεται δελτίο (role 'issuer') ή όταν παραλαμβάνεται (role 'recipient'). Συνδέει το δελτίο με τη μεταφορά ή το τιμολόγιό του και κρατά την τρέχουσα κατάσταση, τον μεταφορέα και το όχημα. Σχεδόν όλα τα fields είναι readonly, γιατί αποτυπώνουν δηλώσεις που έχουν ήδη γίνει. Χρησιμοποιήστε το model για να διαβάσετε την πορεία μιας παράδοσης, για να προσθέσετε δικά σας fields ή για να αντιδράτε στις αλλαγές κατάστασης μέσω του _l10n_gr_on_status_change. Μη δημιουργείτε διακινήσεις και μη γράφετε στα readonly fields από τον κώδικά σας.
Παράδειγμα
Το σύστημα αποθήκης κρατά τη δική του αναφορά αποστολής σε κάθε δελτίο και αντλεί τα δελτία που δεν έχουν ολοκληρωθεί.
# depends: ['l10n_gr_nt_movement']
# Customer need: the 3PL's WMS keeps its own shipment reference on each delivery note
# The l10n.gr API is reached through the ORM (_inherit), never by importing from odoo.addons.l10n_gr_*
from odoo import api, fields, models
class L10nGrMovement(models.Model):
_inherit = 'l10n_gr.movement'
acme_wms_ref = fields.Char(string='WMS Shipment', copy=False, index=True)
@api.model
def acme_open_issued(self, company):
"""Issued delivery notes not yet final, for the WMS sync (2 cancelled and 8 completed are final)."""
return self.search([
('company_id', '=', company.id),
('role', '=', 'issuer'),
('status_id.code', 'not in', (2, 8)),
], order='id desc')l10n_gr.movement._l10n_gr_match_product
_l10n_gr_match_product(self, code, name) -> product.product (possibly empty)Αντιστοίχιση ειδών στην παραλαβή. Στην πλευρά του λήπτη, επιστρέφει το είδος που αντιστοιχεί σε μια γραμμή του ληφθέντος δελτίου, με βάση τον κωδικό και την περιγραφή της. Δοκιμάζει με τη σειρά τον κωδικό του προμηθευτή της διακίνησης, το barcode, τον εσωτερικό κωδικό και τέλος το ακριβές όνομα, και επιστρέφει κενό recordset όταν δεν βρει τίποτα. Καλείται σε μία διακίνηση. Κάντε override για να προσθέσετε δικό σας κανόνα (παλιό κωδικό, δικό σας field GTIN) και καλέστε το super(), ώστε να ισχύει και η βασική σειρά.
Παράδειγμα
Τα εισερχόμενα δελτία αποστολής με κωδικούς του παλαιού ERP του προμηθευτή αντιστοιχίζονται αυτόματα στα σωστά είδη.
# depends: ['l10n_gr_nt_movement']
# Customer need: suppliers still print the item codes of the old ERP; match them before the standard rules
from odoo import fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
acme_legacy_code = fields.Char(string='Legacy ERP Code', index=True)
class L10nGrMovement(models.Model):
_inherit = 'l10n_gr.movement'
def _l10n_gr_match_product(self, code, name):
self.ensure_one()
if code:
product = self.env['product.product'].search([
('acme_legacy_code', '=', code.strip()),
('company_id', 'in', (False, self.company_id.id)),
], limit=1)
if product:
return product
# standard order: supplier code, barcode, internal reference, exact name
return super()._l10n_gr_match_product(code, name)l10n_gr.movement._l10n_gr_on_status_change
_l10n_gr_on_status_change(self, old, new) -> NoneHook αλλαγής κατάστασης. Καλείται σε μία διακίνηση αμέσως μετά την αλλαγή της κατάστασής της, μόνο όταν ο κωδικός άλλαξε πραγματικά· τα old και new είναι οι ακέραιοι κωδικοί κατάστασης (βλ. status_id). Η σουίτα το χρησιμοποιεί για να ετοιμάσει επιστροφή και δραστηριότητα πιστωτικού μετά από αποτυχημένη, μερική ή απορριφθείσα παράδοση, να κλείσει τις ανοιχτές δραστηριότητες όταν η κατάσταση γίνει τελική, να κλείσει τις δραστηριότητες προθεσμίας και να ειδοποιήσει τον πελάτη στην αναχώρηση. Κάντε override στο δικό σας module για να προσθέσετε τη δική σας αντίδραση και καλέστε πάντα πρώτα το super(), ώστε να τρέξει η λογική της σουίτας. Η δήλωση που προκάλεσε την αλλαγή έχει ήδη γίνει, οπότε κρατήστε τον κώδικα ελαφρύ και μη σηκώνετε exception· ό,τι μπορεί να αποτύχει (email, εξωτερικές κλήσεις) βάλτε το σε savepoint.
Παράδειγμα
Το εξωτερικό WMS ή ο κόμβος EDI ενημερώνεται μόλις μια παράδοση ολοκληρωθεί, αποτύχει ή ξεκινήσει η επιστροφή της.
# depends: ['l10n_gr_nt_movement']
# Customer need: the 3PL's WMS is told at once when a delivery fails, completes or returns
import logging
import requests
from odoo import models
_logger = logging.getLogger(__name__)
NOTIFY = {7: 'failed', 8: 'completed', 9: 'returning'}
class L10nGrMovement(models.Model):
_inherit = 'l10n_gr.movement'
def _l10n_gr_on_status_change(self, old, new):
super()._l10n_gr_on_status_change(old, new) # the suite's own reactions first
url = self.env['ir.config_parameter'].sudo().get_param('acme.wms_webhook_url')
if self.role != 'issuer' or new not in NOTIFY or not url:
return
try: # the declaration is already made: never raise from here
with self.env.cr.savepoint():
requests.post(url, timeout=10, json={
'delivery_note': self.name, 'picking': self.picking_id.name, 'event': NOTIFY[new],
}).raise_for_status()
except Exception:
_logger.exception('WMS notification failed for %s', self.name)l10n_gr.movement.current_vehicle_id
fields.Many2one('l10n_gr.movement.vehicle', string='Current Vehicle', readonly=True)Τρέχον όχημα. Το όχημα που δηλώθηκε τελευταίο για τη διακίνηση, στην αναχώρηση ή σε αλλαγή οχήματος. Μένει κενό όταν η διακίνηση γίνεται χωρίς όχημα του μητρώου, για παράδειγμα με τρίτο μεταφορέα. Διαβάστε το για να δείξετε πού βρίσκονται τα εμπορεύματα ή για να ομαδοποιήσετε τις παραδόσεις ανά όχημα.
Παράδειγμα
Η οθόνη διανομής μετρά τις παραδόσεις που βρίσκονται ακόμη καθ' οδόν ανά φορτηγό του στόλου της εταιρείας.
# depends: ['l10n_gr_nt_movement']
# Customer need: the dispatcher sees how many deliveries each truck is still carrying
from odoo import api, models
class L10nGrMovement(models.Model):
_inherit = 'l10n_gr.movement'
@api.model
def acme_loads_per_vehicle(self):
groups = self._read_group(
[('role', '=', 'issuer'),
('status_id.code', '=', 3), # in transit
('current_vehicle_id', '!=', False),
('company_id', 'in', self.env.companies.ids)],
groupby=['current_vehicle_id'],
aggregates=['__count'],
)
return [{'plate': vehicle.license_plate, 'deliveries': count} for vehicle, count in groups]l10n_gr.movement.delivered_at
fields.Datetime(compute='_compute_l10n_gr_kpi', store=True, string='Delivered')Ώρα παράδοσης. Η στιγμή που επιβεβαιώθηκε για πρώτη φορά αποτέλεσμα παράδοσης: η ώρα της ΑΑΔΕ στο πρώτο επιβεβαιωμένο συμβάν αποτελέσματος ή, αν η ΑΑΔΕ δεν έδωσε ώρα, η ώρα της συσκευής. Συμπληρώνεται με οποιοδήποτε επιβεβαιωμένο αποτέλεσμα, ακόμη και με αποτυχημένη παράδοση· γι' αυτό ελέγξτε και την κατάσταση της διακίνησης όταν θέλετε να ξέρετε αν τα εμπορεύματα παραδόθηκαν πράγματι. Μένει κενό μέχρι να επιβεβαιωθεί αποτέλεσμα. Είναι stored, οπότε μπορείτε να κάνετε αναζήτηση, ταξινόμηση και ομαδοποίηση με αυτό στις δικές σας αναφορές.
Παράδειγμα
Η διοίκηση παρακολουθεί τη συνέπεια των παραδόσεων συγκρίνοντας τον χρόνο παράδοσης με την προγραμματισμένη ημερομηνία της διακίνησης.
# depends: ['l10n_gr_nt_movement']
# Customer need: a punctuality report of deliveries handed over more than a day after the planned date
from datetime import timedelta
from odoo import api, fields, models
class L10nGrMovement(models.Model):
_inherit = 'l10n_gr.movement'
acme_late = fields.Boolean(string='Late Delivery', compute='_compute_acme_late', store=True)
@api.depends('delivered_at', 'status_id', 'picking_id.scheduled_date')
def _compute_acme_late(self):
for movement in self:
planned = movement.picking_id.scheduled_date
# delivered_at is also set by a failed delivery: count only goods handed over (5, 8)
movement.acme_late = bool(
planned and movement.delivered_at
and movement.status_id.code in (5, 8)
and movement.delivered_at > planned + timedelta(days=1)
)l10n_gr.movement.event
_name = 'l10n_gr.movement.event' (Digital Movement Event); fields: movement_id, event_type, state ('queued' | 'sent' | 'confirmed' | 'failed'), outcome ('FULL' | 'PARTIAL' | 'NONE'), vehicle_id, carrier_id, lat, lon, local_ts, aade_ts, origin ('odoo' | 'external'), user_idΣυμβάν διακίνησης. Κάθε βήμα που καταγράφηκε για μια διακίνηση (έκδοση, αναχώρηση ή αλλαγή οχήματος, αποτέλεσμα παράδοσης, απόρριψη, επιστροφή, ακύρωση), με το ποιος το δήλωσε, πότε, από πού και με ποιο αποτέλεσμα. Τα συμβάντα τα γράφει η ροή δηλώσεων της σουίτας και η συσκευή του οδηγού· η δημιουργία ή η αλλαγή τους από δικό σας κώδικα δεν υποστηρίζεται. Διαβάστε τα για χρονολόγια, αναφορές οδηγών ή ανάλυση συνέπειας.
Παράδειγμα
Ο υπεύθυνος logistics λαμβάνει μηνιαία αναφορά με τις πλήρεις, μερικές και αποτυχημένες παραδόσεις ανά οδηγό.
# depends: ['l10n_gr_nt_movement']
# Customer need: a monthly driver report of confirmed delivery outcomes (full, partial, failed)
# Events are read only (the suite and the driver device write them): the report lives on the movement
from dateutil.relativedelta import relativedelta
from odoo import api, models
class L10nGrMovement(models.Model):
_inherit = 'l10n_gr.movement'
@api.model
def acme_driver_month(self, month_start):
groups = self.env['l10n_gr.movement.event']._read_group(
[('state', '=', 'confirmed'),
('outcome', '!=', False),
('local_ts', '>=', month_start),
('local_ts', '<', month_start + relativedelta(months=1))],
groupby=['user_id', 'outcome'],
aggregates=['__count'],
)
report = {}
for user, outcome, count in groups:
report.setdefault(user.name or '-', {'FULL': 0, 'PARTIAL': 0, 'NONE': 0})[outcome] = count
return reportl10n_gr.movement.l10n_gr_track_url
fields.Char(compute='_compute_l10n_gr_track_url', string='Tracking Link') (not stored)Σύνδεσμος παρακολούθησης. Ο δημόσιος σύνδεσμος όπου ο πελάτης παρακολουθεί την παράδοση. Υπάρχει μόνο όταν η διακίνηση έχει access token, που το δημιουργεί η σουίτα όταν στέλνει την ειδοποίηση αναχώρησης με email ή SMS σε πελάτη που την έχει ζητήσει· αλλιώς μένει κενός. Χρησιμοποιήστε τον στα δικά σας email templates, SMS ή σελίδες portal, και ελέγξτε ότι υπάρχει πριν τον στείλετε.
Παράδειγμα
Το email αποστολής του e-shop δίνει στον αγοραστή σύνδεσμο παρακολούθησης της παράδοσης, μόνο όταν ο σύνδεσμος υπάρχει.
<!-- depends: ['l10n_gr_nt_movement'] -->
<!-- Customer need: the e-shop's 'order shipped' email carries the delivery tracking link when there is one -->
<record id="mail_template_acme_shipped" model="mail.template">
<field name="name">Acme: order shipped</field>
<field name="model_id" ref="stock.model_stock_picking"/>
<field name="subject">Your order {{ object.origin }} is on its way</field>
<field name="email_to">{{ object.partner_id.email }}</field>
<field name="body_html" type="html">
<div>
<p>Dear <t t-out="object.partner_id.name"/>,</p>
<p>Your order <t t-out="object.origin"/> has left our warehouse.</p>
<!-- empty until the suite has created the access token: never send a blank link -->
<t t-set="track" t-value="object.l10n_gr_movement_id.l10n_gr_track_url"/>
<p t-if="track">Follow the delivery: <a t-att-href="track">track your order</a></p>
</div>
</field>
</record>l10n_gr.movement.status_id
fields.Many2one('aade.delivery.status', string='Status', readonly=True, tracking=True, index=True)Κατάσταση. Η τρέχουσα κατάσταση του δελτίου στην ΑΑΔΕ. Οι κωδικοί είναι: 1 καταχωρημένο, 2 ακυρωμένο, 3 σε διακίνηση, 4 απορριφθέν, 5 παραδόθηκε από τον μεταφορέα, 7 αποτυχημένη παράδοση, 8 ολοκληρωμένο, 9 σε επιστροφή· τελικές είναι οι 2 και 8. Αλλάζει μόνο μέσα από τις δηλώσεις και τους ελέγχους κατάστασης της σουίτας και κάθε αλλαγή καταγράφεται στο chatter. Διαβάστε το, φιλτράρετε ή ομαδοποιήστε με αυτό· μην το γράφετε.
Παράδειγμα
Οι πωλητές βλέπουν σε κάθε παραγγελία την κατάσταση ΑΑΔΕ των παραδόσεών της χωρίς να ανοίξουν τις διακινήσεις.
# depends: ['l10n_gr_nt_movement', 'sale_stock']
# Customer need: sales staff see on the order whether the goods have reached the customer
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
acme_delivery_status = fields.Char(string='Delivery Status', compute='_compute_acme_delivery_status')
def _compute_acme_delivery_status(self):
# search movements by transfer: picking.l10n_gr_movement_id is not stored
movements = self.env['l10n_gr.movement'].search([
('role', '=', 'issuer'),
('picking_id', 'in', self.picking_ids.ids),
], order='id desc')
for order in self:
mine = movements.filtered(lambda m: m.picking_id in order.picking_ids)
order.acme_delivery_status = ', '.join(mine.mapped('status_id.name')) or Falsel10n_gr.movement.vehicle
_name = 'l10n_gr.movement.vehicle' (Movement Vehicle); _rec_name = 'license_plate'; fields: license_plate (required), transport_type_id, p_number (trailer), carrier_id (owner, default company partner), company_id, activeΜητρώο οχημάτων. Τα οχήματα της εταιρείας που χρησιμοποιούνται στις διακινήσεις, με όνομα την πινακίδα τους. Η πινακίδα αποθηκεύεται με κεφαλαία, χωρίς κενά και παύλες, και είναι μοναδική ανά εταιρεία. Από προεπιλογή το νέο όχημα είναι φορτηγό ιδιωτικής χρήσης (κωδικός μέσου μεταφοράς 2 της ΑΑΔΕ) και ιδιοκτήτης του η εταιρεία. Μπορείτε να δημιουργείτε οχήματα από τη δική σας σύνδεση με σύστημα στόλου, να προσθέτετε fields με _inherit και να αρχειοθετείτε όσα δεν χρησιμοποιούνται αντί να τα διαγράφετε, γιατί τα παλαιότερα συμβάντα δείχνουν σε αυτά.
Παράδειγμα
Τα οχήματα του Στόλου δημιουργούνται ή αρχειοθετούνται αυτόματα ως οχήματα διακίνησης.
# depends: ['l10n_gr_nt_movement', 'fleet']
# Customer need: trucks registered in Fleet become movement vehicles without retyping the plate
from odoo import fields, models
class FleetVehicle(models.Model):
_inherit = 'fleet.vehicle'
acme_movement_vehicle_id = fields.Many2one('l10n_gr.movement.vehicle', copy=False, readonly=True)
def acme_sync_movement_vehicle(self):
Vehicle = self.env['l10n_gr.movement.vehicle'].with_context(active_test=False)
for car in self.filtered('license_plate'):
company = car.company_id or self.env.company
# the register stores plates in capitals without spaces or dashes
plate = car.license_plate.upper().replace(' ', '').replace('-', '')
vehicle = Vehicle.search([('license_plate', '=', plate), ('company_id', '=', company.id)], limit=1)
if not vehicle:
vehicle = Vehicle.create({'license_plate': plate, 'company_id': company.id})
vehicle.active = car.active # archive, never delete: past events refer to it
car.acme_movement_vehicle_id = vehiclel10n_gr.route.load.line
_name = 'l10n_gr.route.load.line' (Route Load Line); fields: batch_id (required), product_id (storable), uom_id, quantity (loaded), sold_qty, returned_qty, available_qty (computed)Γραμμή φορτίου. Τι φορτώθηκε σε δρομολόγιο πώλησης επί αυτοκινήτου, ανά είδος, μαζί με όσα πουλήθηκαν στις στάσεις, όσα επέστρεψαν στην αποθήκη και όσα απομένουν στο όχημα (φορτωμένα μείον πωλημένα μείον επιστροφές). Οι πωλημένες και οι επιστρεφόμενες ποσότητες υπολογίζονται από τις στάσεις και τις επιστροφές του δρομολογίου και δεν αποθηκεύονται. Διαβάστε το για οθόνες αποθέματος οχήματος ή αναφορές τέλους ημέρας.
Παράδειγμα
Το γραφείο λαμβάνει στο τέλος της ημέρας σύνοψη με τις ποσότητες που φορτώθηκαν, πωλήθηκαν, επιστράφηκαν και απομένουν σε κάθε δρομολόγιο πώλησης επί αυτοκινήτου.
# depends: ['l10n_gr_nt_movement']
# Customer need: at the end of the day the office sees what is still on the van, per product
from markupsafe import Markup
from odoo import _, models
class StockPickingBatch(models.Model):
_inherit = 'stock.picking.batch'
def acme_post_van_stock(self):
for route in self.filtered(lambda r: r.l10n_gr_route_kind == 'van_sales'):
lines = self.env['l10n_gr.route.load.line'].search([('batch_id', '=', route.id)])
rows = [
_('%(product)s: loaded %(loaded)s, sold %(sold)s, returned %(ret)s, on van %(left)s %(uom)s',
product=line.product_id.display_name, loaded=line.quantity, sold=line.sold_qty,
ret=line.returned_qty, left=line.available_qty, uom=line.uom_id.name)
for line in lines if line.available_qty
]
body = Markup('<br/>').join(rows) if rows else _('Nothing left on the van.')
route.message_post(body=body)stock.picking.batch.l10n_gr_route_kind
fields.Selection([('distribution', 'Distribution'), ('van_sales', 'Van Sales')], string='Route Type')Είδος δρομολογίου. Ορίζει ότι ένα batch είναι δρομολόγιο και τι είδους. Διανομή: ένα δελτίο ανά στάση, που εκδίδεται πριν την αναχώρηση. Πώληση επί αυτοκινήτου: ένα συγκεντρωτικό δελτίο για όλο το φορτίο, με το οποίο συσχετίζεται κάθε πώληση σε στάση. Κενό σημαίνει απλό batch, που η σουίτα δεν το χειρίζεται ως δρομολόγιο. Ορίστε το όταν δημιουργείτε το δρομολόγιο από το δικό σας module προγραμματισμού και μην το αλλάζετε αφού φορτωθεί.
Παράδειγμα
Ένα module προγραμματισμού δημιουργεί δρομολόγια διανομής από τις έτοιμες παραδόσεις της ημέρας, ώστε η σουίτα να εκδίδει ένα δελτίο αποστολής ανά στάση.
# depends: ['l10n_gr_nt_movement']
# Customer need: the route planner turns the day's ready deliveries into a distribution route in one click
from odoo import Command, _, models
from odoo.exceptions import UserError
class StockPicking(models.Model):
_inherit = 'stock.picking'
def acme_create_distribution_route(self):
pickings = self.filtered(
lambda p: p.picking_type_code == 'outgoing' and p.state == 'assigned' and not p.batch_id)
if not pickings:
raise UserError(_('No ready deliveries to plan.'))
if len(pickings.company_id) > 1:
raise UserError(_('Plan one company at a time.'))
# set the kind at creation; never change it once the route is loaded
return self.env['stock.picking.batch'].create({
'l10n_gr_route_kind': 'distribution',
'company_id': pickings.company_id.id,
'picking_ids': [Command.set(pickings.ids)],
})stock.picking.l10n_gr_movement_id
fields.Many2one('l10n_gr.movement', compute='_compute_l10n_gr_movement', string='Digital Movement') (not stored)Διακίνηση της μεταφοράς. Η ψηφιακή διακίνηση του δελτίου που εκδόθηκε για αυτή τη μεταφορά, ή κενό. Είναι computed και όχι stored: διαβάστε το σε εγγραφή, αλλά μην το χρησιμοποιείτε σε domain αναζήτησης· για αναζήτηση, ψάξτε στο l10n_gr.movement με βάση τη μεταφορά.
Παράδειγμα
Το μήνυμα EDI αποστολής προς αλυσίδα λιανικής περιλαμβάνει τον αριθμό και την κατάσταση του δελτίου αποστολής της διακίνησης.
# depends: ['l10n_gr_nt_movement']
# Customer need: the supermarket chain's EDI despatch advice must carry the AADE delivery-note number
from odoo import models
class StockPicking(models.Model):
_inherit = 'stock.picking'
def acme_edi_desadv_payload(self):
self.ensure_one()
movement = self.l10n_gr_movement_id # computed, not stored: read it, never search on it
return {
'despatch_number': self.name,
'order_reference': self.origin,
'delivery_note': movement.name or None,
'aade_status': movement.status_id.code or None,
'vehicle': movement.current_vehicle_id.license_plate or None,
'lines': [
{'gtin': m.product_id.barcode, 'qty': m.quantity, 'uom': m.product_uom.name}
for m in self.move_ids
],
}stock.picking.l10n_gr_route_id
fields.Many2one('stock.picking.batch', string='Van Sales Route', index=True, copy=False, domain=van-sales routes in transit)Δρομολόγιο πώλησης επί αυτοκινήτου. Το δρομολόγιο πώλησης επί αυτοκινήτου στο οποίο ανήκει μια πώληση σε στάση· το δελτίο της συσχετίζεται με το συγκεντρωτικό δελτίο του δρομολογίου. Η στάση συνδέεται μέσω αυτού του field και δεν γίνεται μέλος του batch. Το συμπληρώνει η ροή της πώλησης επί αυτοκινήτου· διαβάστε το για αναφορές ανά δρομολόγιο και μην το αλλάζετε αφού εκδοθεί το δελτίο της στάσης.
Παράδειγμα
Κάθε δρομολόγιο πώλησης επί αυτοκινήτου εμφανίζει πόσες πωλήσεις ολοκληρώθηκαν στις στάσεις του, για σύγκριση μεταξύ δρομολογίων.
# depends: ['l10n_gr_nt_movement']
# Customer need: the sales manager compares how many stops each van-sales route actually served
from odoo import fields, models
class StockPickingBatch(models.Model):
_inherit = 'stock.picking.batch'
acme_stop_count = fields.Integer(string='Stops Served', compute='_compute_acme_stop_count')
def _compute_acme_stop_count(self):
# stops are linked through l10n_gr_route_id, not as members of the batch
counts = dict(self.env['stock.picking']._read_group(
[('l10n_gr_route_id', 'in', self.ids), ('state', '=', 'done')],
groupby=['l10n_gr_route_id'],
aggregates=['__count'],
))
for route in self:
route.acme_stop_count = counts.get(route, 0)Ψηφιακή διακίνηση, εφαρμογή οδηγού
depends: ['l10n_gr_nt_movement_mobile']
Η συσκευή του οδηγού για την ψηφιακή διακίνηση: δρομολόγια ανά οδηγό, δηλώσεις αναχώρησης και παράδοσης επί τόπου και αποδεικτικό παράδοσης με υπογραφή, φωτογραφίες και θέση. Απαιτεί το l10n_gr_nt_movement_mobile (με τα l10n_gr_nt_movement και hr).
l10n_gr.movement.proof
_name = 'l10n_gr.movement.proof' (Delivery Proof); fields: movement_id, event_id, employee_id (driver), outcome (related to the event), signature (Image), signer_name, signer_role, photo_ids, note, lat, lon, accuracy, distance_m, without_position, local_ts, map_urlΑποδεικτικό παράδοσης. Ό,τι συνέλεξε ο οδηγός στην παράδοση: την υπογραφή, το όνομα και την ιδιότητα του παραλήπτη, φωτογραφίες, σημείωση, τη θέση της συσκευής με την ακρίβειά της και την απόσταση από τη διεύθυνση παράδοσης, ή ένδειξη ότι δεν υπήρχε θέση. Το δημιουργεί η συσκευή του οδηγού μαζί με το συμβάν παράδοσης. Διαβάστε το για να επισυνάψετε το αποδεικτικό σε email προς τον πελάτη, να το δείξετε στο portal ή να ελέγξετε παραδόσεις μακριά από τη διεύθυνση.
Παράδειγμα
Ένας ημερήσιος έλεγχος επισημαίνει παραδόσεις με αποδεικτικό που υπογράφηκε μακριά από τη διεύθυνση του πελάτη ή χωρίς θέση συσκευής.
# depends: ['l10n_gr_nt_movement_mobile']
# Customer need: the logistics manager reviews deliveries signed far from the address or without a position
# Proofs are read only (the driver device creates them): the check lives on the movement
from datetime import timedelta
from odoo import _, api, fields, models
class L10nGrMovement(models.Model):
_inherit = 'l10n_gr.movement'
@api.model
def _cron_acme_flag_far_deliveries(self, max_distance=500):
proofs = self.env['l10n_gr.movement.proof'].search([
('local_ts', '>=', fields.Datetime.now() - timedelta(days=1)),
'|', ('without_position', '=', True), ('distance_m', '>', max_distance),
])
for proof in proofs:
where = _('no position') if proof.without_position else _('%s m from the address', proof.distance_m)
proof.movement_id.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Check delivery proof'),
note=_('Signed by %(name)s, %(where)s.', name=proof.signer_name or '-', where=where),
user_id=proof.movement_id.picking_id.user_id.id or self.env.user.id,
)stock.picking.batch.l10n_gr_driver_id
fields.Many2one('hr.employee', string='Driver', index=True, check_company=True)Οδηγός δρομολογίου. Ο οδηγός που βλέπει το δρομολόγιο στη συσκευή του. Όταν είναι κενό, το βλέπει κάθε οδηγός του οχήματος και το αναλαμβάνει όποιος δηλώσει πρώτος την αναχώρηση. Συμπληρώστε το από το δικό σας module προγραμματισμού πριν φύγει το δρομολόγιο· ο εργαζόμενος πρέπει να ανήκει στην εταιρεία του δρομολογίου.
Παράδειγμα
Το module διανομής αναθέτει κάθε δρομολόγιο σε συγκεκριμένο οδηγό, ώστε μόνο αυτός να το βλέπει στη συσκευή του.
# depends: ['l10n_gr_nt_movement_mobile']
# Customer need: dispatch assigns each route to the driver on shift before the van leaves
from odoo import _, models
from odoo.exceptions import UserError
class StockPickingBatch(models.Model):
_inherit = 'stock.picking.batch'
def acme_assign_driver(self, employee):
for route in self:
if not route.l10n_gr_route_kind:
raise UserError(_('%s is not a route.', route.name))
if employee.company_id != route.company_id:
raise UserError(_('%(driver)s does not work for %(company)s.',
driver=employee.name, company=route.company_id.name))
# only this driver sees the routes on the driver device
self.l10n_gr_driver_id = employeeΦορολογική αναμόρφωση και έλεγχος Ε3
depends: ['l10n_gr_nt_mydata_adjust']
Η Κατάσταση Φορολογικής Αναμόρφωσης από τα βιβλία και ο έλεγχος των ποσών του Ε3 έναντι όσων διαβιβάστηκαν στο myDATA, ανά εταιρεία και χρήση. Απαιτεί το l10n_gr_nt_mydata_adjust (με τα l10n_gr_nt_mydata_invoice και l10n_gr_nt_chart_ext).
account.account.l10n_gr_kfa_line_id
fields.Many2one('l10n_gr.kfa.line', string='Tax add-back statement line', domain=[('is_total', '=', False)])Γραμμή αναμόρφωσης του λογαριασμού. Για λογαριασμό εξόδου με ανάλυση φορολογικής αναμόρφωσης, η γραμμή της Κατάστασης Φορολογικής Αναμόρφωσης στην οποία καταχωρούνται τα άρθρα αναμόρφωσης του λογαριασμού, για παράδειγμα ο κωδικός 2011 για τις δαπάνες ψυχαγωγίας. Η γραμμή του συνόλου δεν επιλέγεται, και το field είναι υποχρεωτικό μόλις ενεργοποιηθεί η ανάλυση στον λογαριασμό. Η κατάσταση αθροίζει τα καταχωρημένα άρθρα αναμόρφωσης, οπότε μια αλλαγή εδώ ισχύει για τα άρθρα που δημιουργούνται μετά και όχι για όσα έχουν ήδη καταχωρηθεί. Ορίστε το στα data ή στον κώδικα εγκατάστασης του module σας όταν προσθέτετε δικούς σας λογαριασμούς εξόδων.
Παράδειγμα
Οι λογαριασμοί εξόδων που προσθέτει ο συνεργάτης για τον πελάτη συνδέονται κατά την εγκατάσταση με τη σωστή γραμμή της Κατάστασης Φορολογικής Αναμόρφωσης.
# depends: ['l10n_gr_nt_mydata_adjust']
# Customer need: the customer's own entertainment sub-accounts feed line 2011 of the tax add-back statement
# __manifest__.py: 'post_init_hook': 'post_init_hook'
def post_init_hook(env):
kfa_line = env['l10n_gr.kfa.line'].search([('code', '=', '2011'), ('is_total', '=', False)], limit=1)
if not kfa_line:
return
accounts = env['account.account'].search([
('code', '=like', '64.98.05.%'), # the customer's entertainment sub-accounts
('l10n_gr_kfa_line_id', '=', False),
])
# applies to add-back entries created from now on, not to entries already posted
accounts.write({'l10n_gr_kfa_line_id': kfa_line.id})l10n_gr.e3.check
_name = 'l10n_gr.e3.check'; one per (year, company_id); fields: line_ids, status, income_book, income_mydata, income_limit, income_breach, expense_book, expense_mydata, expense_limit, expense_breach, expense_pending_aade, adjustment_move_ids, computed_on; action_refresh(self) -> TrueΈλεγχος Ε3 έναντι myDATA. Συγκρίνει τα έσοδα και τα έξοδα των βιβλίων με όσα έχει το myDATA για τη χρήση, στο σύνολο κάθε πλευράς. Έσοδα βιβλίων κάτω από το 70% των διαβιβασμένων ή έξοδα βιβλίων πάνω από το 130% των διαβιβασμένων σημειώνονται ως υπέρβαση, γιατί η δήλωση θα απορριφθεί. Όσο κάποιο καταχωρημένο παραστατικό αγορών της χρήσης δεν έχει ΜΑΡΚ καταχωρημένο στο Odoo, η πλευρά των εξόδων δεν κρίνεται (expense_pending_aade). Το action_refresh() ξαναδιαβάζει και τις δύο πλευρές. Τις τακτοποιητικές εγγραφές τις δημιουργεί ο λογιστής από την οθόνη· αφήστε το βήμα αυτό σε εκείνον και περιοριστείτε στην ανάγνωση του adjustment_move_ids.
Παράδειγμα
Ένας προγραμματισμένος έλεγχος ανανεώνει τη σύγκριση Ε3 με το myDATA και δημιουργεί εργασία για τον λογιστή όταν η δήλωση θα απορριπτόταν.
# depends: ['l10n_gr_nt_mydata_adjust']
# Customer need: before the income tax return, the accountant is warned when books and myDATA disagree
from odoo import _, api, fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
@api.model
def _cron_acme_e3_watch(self):
accountants = self.env.ref('account.group_account_manager').all_user_ids
checks = self.env['l10n_gr.e3.check'].search([('year', '=', fields.Date.today().year - 1)])
for check in checks:
check.action_refresh()
expense_judged = not check.expense_pending_aade
if not (check.income_breach or (expense_judged and check.expense_breach)):
continue
user = accountants.filtered(lambda u: check.company_id in u.company_ids)[:1]
# settlement entries stay with the accountant, from the screen
check.company_id.partner_id.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('E3 vs myDATA %s', check.year),
note=check.status,
user_id=(user or self.env.user).id,
)l10n_gr.kfa.line
_name = 'l10n_gr.kfa.line'; fields: code (unique), name, line_group, form_number, is_total, e3_code, e3_reconcile_code, legal_basis, activeΓραμμές της κατάστασης αναμόρφωσης. Οι γραμμές του εντύπου της ΑΑΔΕ, όπως τις ορίζει ο νόμος: το code είναι η ταυτότητα της γραμμής και το form_number η θέση της στο χαρτί, που δεν ακολουθούν την ίδια σειρά. Μία γραμμή συνόλου (is_total) αθροίζει τις υπόλοιπες και μεταφέρεται στο Ε3. Οι γραμμές φορτώνονται με noupdate, ώστε ο λογιστής να προσθέτει νέα περίπτωση από την οθόνη χωρίς να περιμένει νέα έκδοση. Διαβάστε τις ή αναζητήστε με code· μην αλλάζετε και μη διαγράφετε τις γραμμές που έρχονται με τη σουίτα· όσες δεν χρειάζεστε, αρχειοθετήστε τες.
Παράδειγμα
Κατά τη μετάπτωση, η αντιστοίχιση λογαριασμών εξόδων με γραμμές αναμόρφωσης, όπως υπήρχε στο παλαιό ERP, φορτώνεται με βάση τον κωδικό γραμμής.
# odoo-bin shell snippet, run through the console (staging first)
# depends: ['l10n_gr_nt_mydata_adjust']
# Customer need: carry the legacy ERP's add-back mapping (account -> form line code) into Odoo
import csv
import io
MAPPING = """account;kfa
64.98.05.0000;2011
""" # exported from the legacy ERP, one row per expense account
for row in csv.DictReader(io.StringIO(MAPPING), delimiter=';'):
line = env['l10n_gr.kfa.line'].search([('code', '=', row['kfa']), ('is_total', '=', False)], limit=1)
account = env['account.account'].search([('code', '=', row['account'])], limit=1)
if not (line and account):
print('skipped', row)
continue
account.l10n_gr_kfa_line_id = line
env.cr.commit()l10n_gr.kfa.report
_name = 'l10n_gr.kfa.report'; one per (year, company_id); fields: line_ids, amount_total, e3_amount, e3_reconcile_amount, e3_matches, e3_status, l10n_gr_pending_count, withholding_declared, computed_on; action_compute(self) -> TrueΚατάσταση Φορολογικής Αναμόρφωσης. Η κατάσταση μίας εταιρείας για μία χρήση (μία εγγραφή ανά χρήση και εταιρεία), με προεπιλογή την προηγούμενη χρήση, γιατί συνοδεύει τη δήλωση φορολογίας εισοδήματος. Το action_compute() ξαναδιαβάζει τα καταχωρημένα άρθρα αναμόρφωσης και ξαναχτίζει όλες τις γραμμές του εντύπου, και τις μηδενικές. Δίνει το σύνολο του εντύπου, το ποσό για τον κωδικό 455 του Ε3, το ποσό συμφωνίας από τους λογαριασμούς τάξεως, αν τα δύο συμφωνούν και πόσα καταχωρημένα παραστατικά της χρήσης χρεώνουν δαπάνη με ανάλυση αναμόρφωσης χωρίς να έχουν ακόμη άρθρο αναμόρφωσης. Διαβάστε το για δικές σας αναφορές ή καλέστε το action_compute() για ανανέωση.
Παράδειγμα
Το πακέτο κλεισίματος χρήσης για τη διοίκηση περιλαμβάνει το σύνολο της αναμόρφωσης, το ποσό του Ε3 και τα παραστατικά που δεν έχουν ακόμη εγγραφή αναμόρφωσης.
# depends: ['l10n_gr_nt_mydata_adjust']
# Customer need: the CFO's year-end pack shows the add-back total, E3 box 455 and whether they agree
from odoo import _, models
from odoo.exceptions import UserError
class ResCompany(models.Model):
_inherit = 'res.company'
def acme_kfa_year_summary(self, year):
self.ensure_one()
report = self.env['l10n_gr.kfa.report'].search(
[('year', '=', year), ('company_id', '=', self.id)], limit=1)
if not report:
raise UserError(_('No tax add-back statement for %s yet.', year))
report.action_compute()
return {
'total': report.amount_total,
'e3_455': report.e3_amount,
'off_balance': report.e3_reconcile_amount,
'agrees': report.e3_matches,
'note': report.e3_status,
'documents_without_add_back': report.l10n_gr_pending_count,
}Βασικά δεδομένα myDATA
depends: ['l10n_gr_nt_mydata_base']
Τα δεδομένα αναφοράς και οι ρυθμίσεις στις οποίες στηρίζεται κάθε παραστατικό myDATA: λογιστικές κατηγορίες για τον χαρακτηρισμό, τρόποι πληρωμής και μονάδες της ΑΑΔΕ, κανόνες παρακρατήσεων και τελών και ρυθμίσεις εταιρείας, όπως το όριο μετρητών στη λιανική. Απαιτεί το l10n_gr_nt_mydata_base (το φέρνει κάθε module τιμολόγησης της σουίτας).
aade.accounting.category
_name = 'aade.accounting.category' (Accounting category); fields: name, company_id, product_type ('consu' | 'service'), preferred_classification_ids, preferred_expense_classification_idsΛογιστική κατηγορία. Ομαδοποιεί τα είδη για τον χαρακτηρισμό στο myDATA: κάθε κατηγορία, ανά εταιρεία, ορίζει τους χαρακτηρισμούς εσόδων και εξόδων που προσφέρονται στις γραμμές των ειδών της. Είδος χωρίς κατηγορία παίρνει την πρώτη κατηγορία του τύπου του. Ο χαρακτηρισμός που γράφεται τελικά στη γραμμή βγαίνει από τον λογαριασμό της· η κατηγορία ορίζει μόνο ποιες επιλογές προσφέρονται. Δημιουργήστε κατηγορίες που ταιριάζουν στον κατάλογο του πελάτη και αντιστοιχίστε τες στα είδη· τους χαρακτηρισμούς τους ορίζει ο λογιστής πάνω στην κατηγορία.
Παράδειγμα
Τα είδη ομαδοποιούνται σε λογιστικές κατηγορίες που ταιριάζουν στον κατάλογο του πελάτη, ώστε οι γραμμές των τιμολογίων να προτείνουν τους σωστούς χαρακτηρισμούς.
# depends: ['l10n_gr_nt_mydata_base']
# Customer need: a furniture maker separates own production from traded goods so the right income classifications are offered
# __manifest__.py: 'post_init_hook': 'post_init_hook'
CATEGORIES = [
('Own production', 'consu', 'Furniture'),
('Traded goods', 'consu', 'Accessories'),
('Installation services', 'service', 'Services'),
]
def post_init_hook(env):
company = env.company
for name, ptype, product_categ in CATEGORIES:
category = env['aade.accounting.category'].search(
[('name', '=', name), ('company_id', '=', company.id)], limit=1)
if not category:
category = env['aade.accounting.category'].create(
{'name': name, 'product_type': ptype, 'company_id': company.id})
env['product.template'].search([('categ_id.name', '=', product_categ)]).write(
{'aade_accounting_category_id': category.id})
# the accountant sets each category's classifications from the screenaade.payment.method
_name = 'aade.payment.method' (myDATA Payment Method (8.12)); fields: code (Integer), nameΤρόπος πληρωμής ΑΑΔΕ. Οι τρόποι πληρωμής του πίνακα 8.12 της ΑΑΔΕ (μετρητά, τράπεζα, κάρτα, επί πιστώσει κ.ά.), που έρχονται ως δεδομένα. Διαβάστε και αναζητήστε με code· μην προσθέτετε και μην αλλάζετε εγγραφές.
Παράδειγμα
Η μετάπτωση από παλαιό ERP μεταφράζει τους κωδικούς πληρωμής του στους τρόπους πληρωμής της ΑΑΔΕ.
# depends: ['l10n_gr_nt_mydata_base']
# Customer need: invoices migrated from the legacy ERP keep how they were paid, as AADE payment methods (table 8.12)
from odoo import api, models
LEGACY_TO_AADE = {
'ΜΕΤ': 3, # cash
'ΤΡΑ': 1, # domestic bank account
'ΚΑΡ': 7, # POS / e-POS
'ΕΠΙ': 4, # cheque
}
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def acme_aade_method_for(self, legacy_code):
code = LEGACY_TO_AADE.get((legacy_code or '').strip().upper(), 5) # unknown: on credit
method = self.env['aade.payment.method'].search([('code', '=', code)], limit=1)
return method # shipped data: read it, never create or change recordsaade.withholding.rule
_name = 'aade.withholding.rule' (Withholding, Fee or Contribution Rule); _inherit = ['l10n_gr.tax.group.owner']; fields: sequence, name, code, active, company_id, rate, base, mydata_slot, mydata_code, threshold_amount, cap_per_document, cap_per_month, effect, tax_id, tax_purchase_id, account_id, payable_account_id, l10n_gr_account_effective_id, legal_basisΚανόνας παρακράτησης. Κανόνας για παρακράτηση, τέλος ή εισφορά, ανά εταιρεία. Ορίζει τον συντελεστή και τη βάση, το field και την κατηγορία του myDATA όπου δηλώνεται το ποσό, προαιρετικό κατώτατο ποσό, ανώτατα όρια ανά παραστατικό και ανά μήνα, και αν το ποσό παρακρατείται στην πληρωμή ή μόνο δηλώνεται. Κάθε ελληνική εταιρεία παίρνει τους βασικούς κανόνες όταν δημιουργείται. Η σουίτα δημιουργεί αυτόματα τους αντίστοιχους φόρους και την ομάδα φόρων του Odoo (tax_id, tax_purchase_id), που οι χρήστες επιλέγουν στις γραμμές. Το l10n_gr_account_effective_id δείχνει τον λογαριασμό όπου θα γραφτεί σήμερα το ποσό. Δημιουργήστε κανόνες για τις περιπτώσεις του πελάτη από data ή κώδικα· μην αλλάζετε απευθείας τους φόρους που παράγονται.
Παράδειγμα
rules = self.env['aade.withholding.rule'].search([('company_id', '=', self.env.company.id), ('active', '=', True)])
rates = {r.code: r.rate for r in rules}payment.method.aade_payment_method_id
fields.Many2one('aade.payment.method', string='AADE Payment Method')Τρόπος ΑΑΔΕ σε τρόπο πληρωμής του Odoo. Συνδέει έναν τρόπο πληρωμής του Odoo με τον τρόπο πληρωμής της ΑΑΔΕ ως τον οποίο δηλώνεται. Ορίστε το στους τρόπους πληρωμής που προσθέτει το module σας.
Παράδειγμα
Ένας νέος τρόπος πληρωμής του e-shop αντιστοιχίζεται στον τρόπο πληρωμής της ΑΑΔΕ με τον οποίο πρέπει να δηλώνεται.
<!-- depends: ['l10n_gr_nt_mydata_base', 'payment'] -->
<!-- Customer need: the IRIS instant-payment method added for the e-shop is declared to AADE as IRIS (code 8) -->
<record id="payment_method_acme_iris" model="payment.method">
<field name="name">IRIS</field>
<field name="code">acme_iris</field>
<field name="image" type="base64" file="acme_custom/static/img/iris.png"/>
<field name="aade_payment_method_id" model="aade.payment.method" search="[('code', '=', 8)]"/>
</record>product.template.aade_accounting_category_id
fields.Many2one('aade.accounting.category', string='Accounting Category')Λογιστική κατηγορία είδους. Η λογιστική κατηγορία του είδους, που ορίζει ποιους χαρακτηρισμούς myDATA προσφέρουν οι γραμμές των παραστατικών του. Κρατήστε τη συμβατή με τον λογαριασμό εσόδων του είδους, γιατί ο λογαριασμός ορίζει τι γράφεται τελικά· η φόρμα του είδους προειδοποιεί όταν τα δύο διαφωνούν. Ορίστε τη όταν εισάγετε ή δημιουργείτε είδη.
Παράδειγμα
Τα είδη που εισάγονται από το παλαιό ERP λαμβάνουν λογιστική κατηγορία βάσει της παλαιάς ομάδας ειδών.
# odoo-bin shell snippet, run through the console (staging first)
# depends: ['l10n_gr_nt_mydata_base']
# Customer need: products migrated from the legacy ERP arrive with their myDATA accounting category
LEGACY_GROUP_TO_CATEGORY = {'ΕΜΠ': 'Traded goods', 'ΠΡΟ': 'Own production', 'ΥΠΗ': 'Installation services'}
ROWS = [('A-1001', 'ΕΜΠ'), ('P-2040', 'ΠΡΟ'), ('S-0003', 'ΥΠΗ')] # (internal ref, legacy group)
categories = {
c.name: c for c in env['aade.accounting.category'].search([('company_id', '=', env.company.id)])
}
for ref, group in ROWS:
product = env['product.template'].search([('default_code', '=', ref)], limit=1)
category = categories.get(LEGACY_GROUP_TO_CATEGORY.get(group))
if not (product and category):
print('skipped', ref, group)
continue
# keep it consistent with the income account: the account decides what is written
product.aade_accounting_category_id = category
env.cr.commit()product.template.l10n_gr_taric_no
fields.Char(string='TARIC Code', size=10)Κωδικός TARIC. Ο κωδικός TARIC του είδους (ονοματολογία της ΕΕ), ακριβώς δέκα ψηφία ή κενός· κάθε άλλη τιμή σηκώνει σφάλμα επικύρωσης. Συνοδεύει τις γραμμές των δελτίων και χρειάζεται κυρίως σε ενδοκοινοτικές και τελωνειακές διακινήσεις.
Παράδειγμα
Ένας εισαγωγέας ενδοκοινοτικών αγαθών φορτώνει τους κωδικούς TARIC από τους τιμοκαταλόγους των προμηθευτών στη δεκαψήφια μορφή που απαιτούν τα παραστατικά.
# depends: ['l10n_gr_nt_mydata_base']
# Customer need: supplier price lists print TARIC codes with dots and spaces; store them clean, report bad ones
import re
from odoo import api, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
@api.model
def acme_set_taric_codes(self, rows):
"""rows: iterable of (internal reference, TARIC as printed). Returns the refs not set."""
skipped = []
for ref, raw in rows:
taric = re.sub(r'\D', '', raw or '')
if len(taric) == 8:
taric += '00' # 8-digit CN code with no TARIC subdivision
product = self.search([('default_code', '=', ref)], limit=1)
if product and len(taric) == 10: # anything else fails the field's validation
product.l10n_gr_taric_no = taric
else:
skipped.append(ref)
return skippedproduct.template.peppol_cpv
fields.Char(string='PEPPOL CPV')Κωδικός CPV. Ο κωδικός CPV του είδους, για παραστατικά προς φορείς του Δημοσίου (B2G).
Παράδειγμα
Ένας προμηθευτής του Δημοσίου δεν μπορεί να επιβεβαιώσει παραγγελία φορέα του Δημοσίου πριν αποκτήσουν όλα τα είδη κωδικό CPV.
# depends: ['l10n_gr_nt_mydata_base', 'sale']
# Customer need: orders for public bodies cannot be confirmed while a product lacks its CPV code
from odoo import _, models
from odoo.exceptions import UserError
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
public = self.env.ref('acme_custom.partner_tag_public_body', raise_if_not_found=False)
for order in self:
if not public or public not in order.partner_id.commercial_partner_id.category_id:
continue
missing = order.order_line.filtered(lambda l: l.product_id and not l.product_id.peppol_cpv)
if missing:
raise UserError(_('Add a CPV code to: %s', ', '.join(missing.product_id.mapped('display_name'))))
return super().action_confirm()res.company.l10n_gr_cash_limit_retail
fields.Monetary(string='Cash limit, retail', default=500.0, currency_field='currency_id')Όριο μετρητών στη λιανική. Το ποσό από το οποίο και πάνω ένα παραστατικό λιανικής (προς ιδιώτη ή επαφή χωρίς ΑΦΜ) εξοφλείται μόνο ηλεκτρονικά. Η σουίτα απορρίπτει παραστατικό πελάτη όταν η συνολική του αξία, με τον ΦΠΑ, φτάνει το όριο και έχει έστω μία γραμμή πληρωμής με μετρητά: το όριο μετριέται σε όλο το παραστατικό και όχι στο ποσό των μετρητών. Με τιμή 0 ο έλεγχος δεν γίνεται. Διαβάστε το στις δικές σας ροές πωλήσεων ή POS, ώστε η ηλεκτρονική πληρωμή να επιλέγεται έγκαιρα.
Παράδειγμα
Οι πωλητές ενημερώνονται ήδη στην προσφορά ότι παραγγελία ιδιώτη ίση ή μεγαλύτερη από το όριο μετρητών πρέπει να εξοφληθεί ηλεκτρονικά.
# depends: ['l10n_gr_nt_mydata_base', 'sale']
# Customer need: sales staff learn at quotation time that a retail order must be paid electronically
from odoo import _, api, models
from odoo.tools import format_amount
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.onchange('order_line', 'partner_id')
def _onchange_acme_cash_limit(self):
company = self.company_id
limit = company.l10n_gr_cash_limit_retail # 0 means the check is off
retail = not self.partner_id.commercial_partner_id.vat
if not limit or not retail or self.currency_id != company.currency_id:
return
if self.amount_total >= limit: # measured on the whole document, VAT included
return {'warning': {
'title': _('Electronic payment required'),
'message': _('Retail orders of %s or more cannot be paid in cash, not even in part.',
format_amount(self.env, limit, company.currency_id)),
}}res.company.l10n_gr_provider_licence
fields.Char(string='ΥΠΑΗΕΣ licence', compute=..., store=False, readonly=True)Αριθμός άδειας παρόχου. Ο αριθμός άδειας λογισμικού του πιστοποιημένου παρόχου ηλεκτρονικής τιμολόγησης μέσω του οποίου εκδίδει η εταιρεία, όπως τυπώνεται στα παραστατικά. Ακολουθεί τη σύνδεση της εταιρείας και είναι readonly. Διαβάστε το μόνο για να το τυπώσετε σε δικές σας εκτυπώσεις.
Παράδειγμα
Μια προσαρμοσμένη διάταξη παραστατικών τυπώνει τον αριθμό άδειας του πιστοποιημένου παρόχου ηλεκτρονικής τιμολόγησης μέσω του οποίου εκδίδει η εταιρεία.
<!-- depends: ['l10n_gr_nt_mydata_base'] -->
<!-- Customer need: the customer's own document layout must still print the provider's licence number -->
<template id="acme_external_layout_licence" inherit_id="web.external_layout_standard">
<xpath expr="//div[@t-field='company.report_footer']" position="after">
<div t-if="company.l10n_gr_provider_licence" class="small text-muted">
Αρ. άδειας ΥΠΑΗΕΣ: <span t-field="company.l10n_gr_provider_licence"/>
</div>
</xpath>
</template>res.company.retail_partner_id
fields.Many2one('res.partner', string='Retail Partner')Επαφή λιανικής. Η ρύθμιση που ορίζει την επαφή λιανικής της εταιρείας. Μια θυγατρική εταιρεία την αντιγράφει από τη μητρική μαζί με τις υπόλοιπες μη απόρρητες ρυθμίσεις myDATA.
Παράδειγμα
Οι ανώνυμες ημερήσιες πωλήσεις από εξωτερικό ταμείο καταχωρίζονται ως πρόχειρα στην επαφή λιανικής της εταιρείας.
# depends: ['l10n_gr_nt_mydata_base']
# Customer need: daily anonymous sales from the shop's external till are booked on the company's retail contact
from odoo import Command, _, api, models
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def acme_create_retail_draft(self, lines, company=None):
"""lines: iterable of (product, quantity, unit price on the same tax basis as the product's taxes)."""
company = company or self.env.company
if not company.retail_partner_id:
raise UserError(_('Set the retail contact of %s first.', company.name))
return self.with_company(company).create({
'move_type': 'out_invoice',
'partner_id': company.retail_partner_id.id,
'invoice_line_ids': [
Command.create({'product_id': p.id, 'quantity': q, 'price_unit': price})
for p, q, price in lines
],
}) # stays draft: the accountant posts itres.partner.l10n_gr_self_invoicing_agreement
fields.Binary(string='Self-billing agreement', attachment=True, copy=False)Συμφωνία αυτοτιμολόγησης. Η υπογεγραμμένη συμφωνία με τον προμηθευτή που επιτρέπει στην εταιρεία να εκδίδει η ίδια τα παραστατικά του. Χωρίς αυτήν η ένδειξη αυτοτιμολόγησης δεν καταχωρείται. Το module σας μπορεί να την επισυνάψει από μια ροή εγγράφων.
Παράδειγμα
Μια εταιρεία που εκδίδει η ίδια τα παραστατικά των προμηθευτών της επισυνάπτει κάθε υπογεγραμμένη συμφωνία από τη ροή εγγράφων και εντοπίζει τους προμηθευτές χωρίς συμφωνία.
# depends: ['l10n_gr_nt_mydata_base']
# Customer need: an olive-oil mill self-bills its farmers; the scanned agreement lands on the farmer
from odoo import _, api, models
from odoo.exceptions import UserError
class ResPartner(models.Model):
_inherit = 'res.partner'
def acme_set_self_billing_agreement(self, attachment):
self.ensure_one()
if attachment.mimetype != 'application/pdf':
raise UserError(_('The self-billing agreement must be a signed PDF.'))
supplier = self.commercial_partner_id
supplier.l10n_gr_self_invoicing_agreement = attachment.datas
supplier.message_post(body=_('Self-billing agreement attached.'))
@api.model
def acme_farmers_without_agreement(self):
tag = self.env.ref('acme_custom.partner_tag_farmer')
return self.search([
('category_id', 'in', tag.ids),
('l10n_gr_self_invoicing_agreement', '=', False),
])uom.uom.aade_unit_id
fields.Many2one('aade.unit.measure', string='AADE Category')Μονάδα ΑΑΔΕ. Αντιστοιχίζει μια μονάδα μέτρησης του Odoo στο είδος ποσότητας της ΑΑΔΕ (πίνακας 8.13). Ορίστε το σε κάθε μονάδα που προσθέτει το module σας και εμφανίζεται σε γραμμές παραστατικών.
Παράδειγμα
Μια νέα μονάδα μέτρησης που χρησιμοποιείται στα τιμολόγια αντιστοιχίζεται στον τύπο ποσότητας της ΑΑΔΕ με τον οποίο δηλώνεται.
<!-- depends: ['l10n_gr_nt_mydata_base', 'uom'] -->
<!-- Customer need: a building-materials trader sells by the pallet of 40 pieces; the unit must map to an AADE quantity type (8.13) -->
<record id="uom_acme_pallet" model="uom.uom">
<field name="name">Pallet (40)</field>
<field name="relative_factor">40</field>
<field name="relative_uom_id" ref="uom.product_uom_unit"/>
<!-- 7: pieces, other cases -->
<field name="aade_unit_id" model="aade.unit.measure" search="[('code', '=', 7)]"/>
</record>l10n.gr — Εξοδολόγια
depends: ['l10n_gr_nt_mydata_expense']
Χαρακτηρίζει τα έξοδα εργαζομένων για το myDATA και τα διαβιβάζει στην ΑΑΔΕ.
hr.expense.classification_category
fields.Selection (the AADE classification categories, e.g. category2_x for expenses), string 'myDATA Expense Category'Κατηγορία χαρακτηρισμού myDATA. Η κατηγορία χαρακτηρισμού της ΑΑΔΕ για το έξοδο. Στη φόρμα του εξόδου η σουίτα τη συμπληρώνει από τον λογαριασμό εξόδου όταν αλλάζει είδος, λογαριασμός, φόροι ή σειρά, και η χειροκίνητη επιλογή μένει όσο δεν αλλάζει κάποιο από αυτά. Η συμπλήρωση αυτή γίνεται μόνο στη φόρμα (onchange)· δεν τρέχει όταν ο κώδικας δημιουργεί ή γράφει έξοδα. Χωρίς αυτήν το έξοδο δεν διαβιβάζεται στο myDATA. Συμπληρώστε την εσείς όταν το module σας δημιουργεί έξοδα (εισαγωγές, κινήσεις καρτών), ώστε να έρχονται ήδη χαρακτηρισμένα, ή χρησιμοποιήστε την για να βρείτε έξοδα που χρειάζονται ακόμη κατηγορία πριν διαβιβαστούν. Γράψτε μόνο έγκυρες τιμές του selection. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_mydata_expense (φέρνει και το hr_expense).
Παράδειγμα
Οι συναλλαγές εταιρικής κάρτας που εισάγονται ως δαπάνες φτάνουν με ήδη συμπληρωμένη την κατηγορία εξόδου myDATA.
# depends: ['l10n_gr_nt_mydata_expense']
# Customer need: company-card transactions imported nightly arrive already classified for myDATA
from odoo import api, fields, models
# the form onchange does not run for code: set the category ourselves (merchant category code -> AADE)
MCC_TO_CATEGORY = {
'5541': 'category2_4', # fuel: general expenses with VAT deduction
'5812': 'category2_5', # restaurants: general expenses without VAT deduction
}
class HrExpense(models.Model):
_inherit = 'hr.expense'
@api.model
def acme_import_card_line(self, employee, product, txn):
return self.create({
'name': txn['merchant'],
'employee_id': employee.id,
'product_id': product.id,
'total_amount_currency': txn['amount'],
'date': fields.Date.to_date(txn['date']),
'payment_mode': 'company_account',
# empty: the user picks the category before the expense can be sent
'classification_category': MCC_TO_CATEGORY.get(txn['mcc'], False),
})Τιμολόγηση myDATA
depends: ['l10n_gr_nt_mydata_invoice']
Προσθέτει στα τιμολόγια και στα παραστατικά αγορών ό,τι χρειάζονται τα ελληνικά παραστατικά: ώρα έκδοσης, γραμμές πληρωμής ΑΑΔΕ, παρακρατήσεις, φόρους σε επίπεδο παραστατικού, σκοπό διακίνησης για παραστατικά που επέχουν θέση δελτίου και χαρακτηρισμό γραμμών. Απαιτεί το l10n_gr_nt_mydata_invoice (με το l10n_gr_nt_mydata_base).
aade.invoice.tax.line
_name = 'aade.invoice.tax.line' (Tax / Withholding Applied to the Invoice); fields: invoice_id, tax_id, base_amount, amount, currency_id, mydata_typeΦόρος σε επίπεδο παραστατικού. Φόρος, τέλος ή παρακράτηση που εφαρμόζεται σε όλο το παραστατικό και όχι σε γραμμή, και επιλέγεται από τους φόρους που έχουν τύπο myDATA. Η βάση του είναι η καθαρή αξία του παραστατικού και το ποσό του είναι είτε το σταθερό ποσό του φόρου είτε ο συντελεστής του επί της καθαρής αξίας των γραμμών. Εμφανίζεται στην καρτέλα φόρων του παραστατικού (account.move.invoice_tax_ids). Διαβάστε το σε αναφορές· η προσθήκη του γίνεται από τη φόρμα του παραστατικού.
Παράδειγμα
Μια προσαρμοσμένη διάταξη τιμολογίου τυπώνει τα τέλη, το χαρτόσημο ή τις παρακρατήσεις που εφαρμόζονται σε ολόκληρο το παραστατικό.
<!-- depends: ['l10n_gr_nt_mydata_invoice'] -->
<!-- Customer need: the customer's invoice layout lists document-level fees and stamp duty under the totals -->
<template id="report_invoice_acme_document_taxes" inherit_id="account.report_invoice_document">
<xpath expr="//div[@id='total']" position="after">
<table t-if="o.invoice_tax_ids" class="table table-sm mt-2">
<thead>
<tr><th>Document tax</th><th class="text-end">Base</th><th class="text-end">Amount</th></tr>
</thead>
<tbody>
<tr t-foreach="o.invoice_tax_ids" t-as="tax_line">
<td t-field="tax_line.tax_id.name"/>
<td class="text-end" t-field="tax_line.base_amount"/>
<td class="text-end" t-field="tax_line.amount"/>
</tr>
</tbody>
</table>
</xpath>
</template>account.move.invoice_time
fields.Datetime(string='Invoice Time', copy=False)Ώρα έκδοσης. Η ώρα έκδοσης του παραστατικού, που είναι υποχρεωτική ένδειξη. Συμπληρώνεται μία φορά, όταν το καταχωρημένο παραστατικό αποκτά την εγγραφή myDATA, και δεν αντιγράφεται ποτέ σε αντιλογισμούς ή αντίγραφα. Αποθηκεύεται σε UTC και τυπώνεται στη ζώνη ώρας της εκδότριας. Διαβάστε τη για αναφορές· μην τη γράφετε.
Παράδειγμα
Μια επιχείρηση λιανικής αναλύει τις πωλήσεις της ανά ώρα έκδοσης, σε τοπική ώρα.
# depends: ['l10n_gr_nt_mydata_invoice']
# Customer need: the shop's BI tool needs sales per hour of issue, in Greek local time
import pytz
from odoo import api, models
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def acme_sales_per_hour(self, date_from, date_to):
tz = pytz.timezone('Europe/Athens') # the issuer's time zone; invoice_time is stored in UTC
moves = self.search([
('move_type', '=', 'out_invoice'), ('state', '=', 'posted'),
('invoice_time', '!=', False),
('invoice_date', '>=', date_from), ('invoice_date', '<=', date_to),
])
hours = {}
for move in moves:
hour = pytz.utc.localize(move.invoice_time).astimezone(tz).hour
hours[hour] = hours.get(hour, 0.0) + move.amount_total_signed
return dict(sorted(hours.items()))account.move.line.classification_category
fields.Selection(selection=CLASSIFICATION_CATEGORY_SELECTION, string='AADE Classification Category', readonly=False)Κατηγορία χαρακτηρισμού. Η κατηγορία χαρακτηρισμού της ΑΑΔΕ για τη γραμμή (για παράδειγμα category1_1 έσοδα από πώληση εμπορευμάτων, category1_3 έσοδα από παροχή υπηρεσιών). Συνήθως προτείνεται από τον λογαριασμό της γραμμής· οι επιλογές που προσφέρονται βγαίνουν από τη λογιστική κατηγορία του είδους, και γραμμή που μένει κενή στην καταχώρηση συμπληρώνεται από τον λογαριασμό της. Διαβάστε τη σε αναφορές ή ορίστε τη σε πρόχειρες γραμμές, όταν η ροή σας γνωρίζει τη σωστή κατηγορία.
Παράδειγμα
Μια προγραμματισμένη ενέργεια επισημαίνει παραστατικά με γραμμές χωρίς χαρακτηρισμό, ώστε ο λογιστής να τα διορθώσει πριν από την προθεσμία του myDATA.
# depends: ['l10n_gr_nt_mydata_invoice']
# Customer need: before the monthly myDATA deadline the accountant gets a to-do for every document with unclassified lines
from dateutil.relativedelta import relativedelta
from odoo import _, api, fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def _cron_acme_missing_classification(self):
summary = _('myDATA classification missing')
start = fields.Date.start_of(fields.Date.today() - relativedelta(months=1), 'month')
lines = self.env['account.move.line'].search([
('parent_state', '=', 'posted'),
('move_id.move_type', 'in', ('out_invoice', 'out_refund', 'in_invoice', 'in_refund')),
('display_type', '=', 'product'),
('date', '>=', start),
('classification_category', '=', False),
])
for move in lines.move_id:
if move.activity_ids.filtered(lambda a: a.summary == summary):
continue
move.activity_schedule('mail.mail_activity_data_todo', summary=summary,
user_id=move.invoice_user_id.id or self.env.user.id)account.move.move_purpose_id
fields.Many2one('aade.move.purpose', string='Movement Purpose', compute='_compute_shipping_info', store=True, readonly=False)Σκοπός διακίνησης. Ο λόγος για τον οποίο διακινούνται τα εμπορεύματα (πίνακας 8.14 της ΑΑΔΕ), για παραστατικά που επέχουν θέση δελτίου αποστολής. Η σουίτα το προτείνει από το παραστατικό και προσφέρει μόνο τους σκοπούς που ταιριάζουν στην κατεύθυνση της διακίνησης· ο χρήστης μπορεί να το αλλάξει. Το module σας μπορεί να το ορίσει στο πρόχειρο πριν την καταχώρηση.
Παράδειγμα
Τα τιμολόγια που επέχουν θέση δελτίου αποστολής για παραγγελίες δειγμάτων φέρουν σκοπό διακίνησης «Δειγματισμός» αντί για «Πώληση».
# depends: ['l10n_gr_nt_mydata_invoice', 'sale']
# Customer need: invoices for orders tagged 'Samples' move the goods with purpose 3 (sampling), not 1 (sale)
from odoo import models
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _create_invoices(self, grouped=False, final=False, date=None):
moves = super()._create_invoices(grouped=grouped, final=final, date=date)
tag = self.env.ref('acme_custom.sale_tag_samples', raise_if_not_found=False)
sampling = self.env['aade.move.purpose'].search([('code', '=', 3)], limit=1)
if not (tag and sampling):
return moves
for move in moves.filtered(lambda m: m.state == 'draft'):
if tag in move.line_ids.sale_line_ids.order_id.tag_ids:
move.move_purpose_id = sampling # draft value: the user can still change it
return movesaccount.move.payment_line_ids
fields.One2many('account.move.payment.line', 'move_id', string='Payment Lines', copy=True)Γραμμές πληρωμής. Ο τρόπος εξόφλησης του παραστατικού, ως τρόποι πληρωμής της ΑΑΔΕ με ποσά (κάθε γραμμή έχει aade.payment.method και ποσό). Οι γραμμές είναι μόνο δήλωση και δεν δημιουργούν εγγραφές στα βιβλία· η πραγματική είσπραξη καταχωρείται όπως πάντα με το «Πληρωμή» του Odoo. Χωρίς γραμμές, όλο το ποσό δηλώνεται επί πιστώσει. Η δική σας ροή πωλήσεων ή e-shop μπορεί να τις συμπληρώνει στο πρόχειρο πριν την καταχώρηση· οι γραμμές μετρητών σε παραστατικά λιανικής ελέγχονται με το όριο μετρητών της εταιρείας.
Παράδειγμα
Τα τιμολόγια των παραγγελιών του e-shop συμπληρώνονται με τον τρόπο πληρωμής της ΑΑΔΕ που χρησιμοποίησε πράγματι ο πελάτης.
# depends: ['l10n_gr_nt_mydata_invoice', 'sale']
# Customer need: e-shop invoices declare how the customer actually paid instead of 'on credit'
from odoo import Command, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _create_invoices(self, grouped=False, final=False, date=None):
moves = super()._create_invoices(grouped=grouped, final=final, date=date)
for move in moves.filtered(lambda m: m.state == 'draft' and not m.payment_line_ids):
orders = move.line_ids.sale_line_ids.order_id
tx = orders.transaction_ids.filtered(lambda t: t.state == 'done')[:1]
aade_method = tx.payment_method_id.aade_payment_method_id
if aade_method:
# a declaration only: the payment itself is still registered with Pay
move.payment_line_ids = [Command.create({
'aade_payment_method_id': aade_method.id,
'amount': move.amount_total,
})]
return movesaccount.move.received_from_mydata
fields.Boolean(string='Received from myDATA', default=False)Ληφθέν από myDATA. Αληθές στα παραστατικά αγορών που δημιουργήθηκαν από παραστατικά που λήφθηκαν μέσω myDATA. Χρησιμοποιήστε το για φίλτρα ή για να διαφοροποιήσετε τη δική σας λογική σε αυτά· το ορίζει η σουίτα, οπότε μην το γράφετε.
Παράδειγμα
Τα τιμολόγια αγορών που παραλαμβάνονται μέσω myDATA χωρίς αντίστοιχη παραγγελία αγοράς προωθούνται στον υπεύθυνο αγορών για έλεγχο.
# depends: ['l10n_gr_nt_mydata_invoice', 'purchase']
# Customer need: bills that arrive through myDATA without a purchase order go to the buyer for review
from odoo import _, api, models
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def _cron_acme_review_received_bills(self):
bills = self.search([
('received_from_mydata', '=', True), # set by the suite, never written here
('move_type', '=', 'in_invoice'),
('state', '=', 'draft'),
'!', ('invoice_line_ids.purchase_line_id', '!=', False), # no line linked to a PO
('activity_ids', '=', False),
])
for bill in bills:
bill.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Bill from myDATA without purchase order'),
user_id=bill.invoice_user_id.id or self.env.user.id,
)account.move.withholding_line_ids
fields.One2many('l10n_gr.withholding.line', 'move_id', string='Withholdings', copy=False)Παρακρατήσεις. Οι παρακρατήσεις επί της αμοιβής, μία γραμμή ανά aade.withholding.rule, με τη βάση, το ποσό και ένδειξη αν εφαρμόστηκε ανώτατο όριο. Ο χρήστης επιλέγει ποιοι κανόνες ισχύουν και η σουίτα υπολογίζει τα ποσά. Διαβάστε τες για βεβαιώσεις ή ειδοποιήσεις πληρωμής· δεν μεταφέρονται όταν αντιγράφεται το παραστατικό.
Παράδειγμα
Στο τέλος του έτους, κάθε ελεύθερος επαγγελματίας λαμβάνει βεβαίωση αμοιβών και παρακρατηθέντος φόρου ανά κανόνα παρακράτησης.
# depends: ['l10n_gr_nt_mydata_invoice']
# Customer need: at year end each freelancer gets a statement of the tax withheld from their fees
from odoo import models
class ResPartner(models.Model):
_inherit = 'res.partner'
def acme_withholding_statement(self, year):
self.ensure_one()
bills = self.env['account.move'].search([
('commercial_partner_id', '=', self.commercial_partner_id.id),
('move_type', '=', 'in_invoice'),
('state', '=', 'posted'),
('date', '>=', f'{year}-01-01'), ('date', '<=', f'{year}-12-31'),
])
totals = {}
for line in bills.withholding_line_ids:
row = totals.setdefault(line.rule_id.name, {'base': 0.0, 'amount': 0.0})
row['base'] += line.base_amount
row['amount'] += line.amount
return totalsaccount.move.withholding_total
fields.Monetary(string='Total Withheld', compute='_compute_withholding_total', store=True, currency_field='currency_id')Σύνολο παρακρατήσεων. Το σύνολο των παρακρατήσεων του παραστατικού, από όποια πηγή κι αν προέρχονται: οι γραμμές παρακράτησης μαζί με τους φόρους παρακράτησης στις γραμμές του τιμολογίου. Είναι stored, οπότε μπορείτε να το αθροίζετε και να ομαδοποιείτε σε αναφορές.
Παράδειγμα
Ο λογιστής βλέπει ανά μήνα πόσο φόρο παρακράτησε η εταιρεία από προμηθευτές και πόσο της παρακράτησαν οι πελάτες της.
# depends: ['l10n_gr_nt_mydata_invoice']
# Customer need: the accountant plans the monthly payment of withheld tax (bills) and tracks what customers withheld (invoices)
from odoo import api, models
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def acme_withheld_per_month(self, year):
groups = self._read_group(
[('move_type', 'in', ('in_invoice', 'out_invoice')),
('state', '=', 'posted'),
('date', '>=', f'{year}-01-01'), ('date', '<=', f'{year}-12-31'),
('withholding_total', '!=', 0)],
groupby=['move_type', 'date:month'],
aggregates=['withholding_total:sum'],
)
# in_invoice: withheld by us, to pay; out_invoice: withheld by customers
return [(move_type, month.strftime('%m/%Y'), total) for move_type, month, total in groups]l10n.gr — Λιανική: Διαβίβαση
depends: ['l10n_gr_nt_mydata_pos']
Συνδέει το Σημείο Πώλησης του Odoo με το myDATA: κάθε απόδειξη εκδίδεται ως ελληνικό φορολογικό παραστατικό.
pos.order.l10n_gr_document_type_name
fields.Char, related to the AADE document type name of the order's accounting document series; not storedΕίδος παραστατικού. Η ονομασία του ελληνικού τύπου παραστατικού της παραγγελίας, για παράδειγμα απόδειξη λιανικής ή τιμολόγιο, όπως προκύπτει από τη σειρά του λογιστικού παραστατικού της παραγγελίας. Μένει κενό μέχρι η παραγγελία να αποκτήσει αυτό το παραστατικό. Χρησιμοποιήστε το για να δείχνετε το είδος του παραστατικού σε δικές σας αποδείξεις, αναφορές ή εξαγωγές, ή για να χωρίζετε τις παραγγελίες ανά είδος σε σύνοψη βάρδιας. Είναι related και όχι αποθηκευμένο: μπορείτε να το διαβάσετε ή να κάνετε αναζήτηση με αυτό, όχι όμως ομαδοποίηση με read_group ή στις λίστες· για ομαδοποίηση, χρησιμοποιήστε Python. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_mydata_pos.
Παράδειγμα
Ο ιδιοκτήτης του καταστήματος θέλει το σημείωμα κλεισίματος κάθε session του Point of Sale να δείχνει πόσες αποδείξεις και πόσα τιμολόγια εκδόθηκαν, με τα σύνολά τους.
# Customer need: the shop owner wants each session's closing note to split receipts from invoices.
# depends: ['point_of_sale', 'l10n_gr_nt_mydata_pos']
# The suite field is reached through the ORM; never import from odoo.addons.l10n_gr_*.
from markupsafe import Markup
from odoo import _, models
class PosSession(models.Model):
_inherit = 'pos.session'
def _acme_totals_by_document_kind(self):
self.ensure_one()
orders = self.order_ids.filtered(lambda o: o.state != 'cancel')
# Related and not stored: group in Python, read_group cannot use it.
return [
(kind or _('No fiscal document yet'), len(group), sum(group.mapped('amount_total')))
for kind, group in orders.grouped('l10n_gr_document_type_name').items()
]
def action_pos_session_closing_control(self, *args, **kwargs):
res = super().action_pos_session_closing_control(*args, **kwargs)
# A cash difference can return a wizard instead of closing: note only closed sessions.
for session in self.filtered(lambda s: s.state == 'closed'):
rows = session._acme_totals_by_document_kind()
session.message_post(body=Markup('<br/>').join('%s: %s / %.2f' % row for row in rows))
return resl10n.gr — Ληφθέντα Παραστατικά
depends: ['l10n_gr_nt_mydata_received']
Φέρνει από το myDATA τα παραστατικά που εξέδωσαν τρίτοι στο ΑΦΜ της εταιρείας, τα κρατά όπως ελήφθησαν, τα συνδέει με τιμολόγιο προμηθευτή και παρακολουθεί την προθεσμία χαρακτηρισμού εξόδων.
account.move.l10n_gr_classification_deadline
fields.Date, computed, stored (depends on invoice_date, received_from_mydata, company_id); empty when the bill did not come from myDATA or has no invoice dateΠροθεσμία χαρακτηρισμού. Για τιμολόγιο προμηθευτή που ήρθε μέσω myDATA, η τελευταία ημέρα για τη διαβίβαση του χαρακτηρισμού εξόδων του: η τελευταία ημέρα του μήνα που ακολουθεί τον μήνα του τιμολογίου. Ο νόμος συνδέει την προθεσμία με τη δήλωση ΦΠΑ της περιόδου (μηνιαία για διπλογραφικά, τριμηνιαία για απλογραφικά)· το field εφαρμόζει τον μηνιαίο κανόνα, που είναι ο αυστηρότερος. Χρησιμοποιήστε το για ταξινόμηση, φίλτρα ή υπενθυμίσεις: πίνακες με τιμολόγια κοντά στην προθεσμία, προγραμματισμένες δραστηριότητες, συγκεντρωτικά email. Είναι αποθηκευμένο, άρα μπαίνει σε domain και σε ομαδοποίηση. Διαβάστε το· μην το γράφετε. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_mydata_received.
Παράδειγμα
Πέντε ημέρες πριν από την προθεσμία, ο λογιστής λαμβάνει υπενθύμιση για κάθε τιμολόγιο αγοράς από το myDATA που δεν έχει ακόμη χαρακτηρισμό εξόδων.
# Customer need: the accountant gets a to-do on every myDATA bill not yet classified, 5 days before its deadline.
# depends: ['l10n_gr_nt_mydata_received']
# Run it from an ir.cron on model account.move, code: model._cron_acme_classification_reminder()
from odoo import _, api, fields, models
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def _cron_acme_classification_reminder(self):
today = fields.Date.context_today(self)
docs = self.env['l10n_gr.mydata.received.doc'].search([
('state', '=', 'matched'), # linked to a bill, not classified yet
('move_id.l10n_gr_classification_deadline', '>=', today),
('move_id.l10n_gr_classification_deadline', '<=', fields.Date.add(today, days=5)),
])
for bill in docs.move_id.filtered(lambda m: not m.activity_ids):
bill.activity_schedule(
'mail.mail_activity_data_todo',
date_deadline=bill.l10n_gr_classification_deadline,
summary=_('Expense classification due'),
user_id=bill.invoice_user_id.id or self.env.user.id,
)l10n_gr.mydata.received.doc
models.Model, _inherit mail.thread + mail.activity.mixin; key fields: company_id (Many2one res.company), state (Selection: new, matched, classified, ignored, cancelled; tracked), move_id (Many2one account.move, tracked), partner_id (Many2one res.partner), issue_date (Date), issuer_vat, issuer_name, series, aa (Char), total_net, total_vat, total_gross (Monetary), line_ids (One2many l10n_gr.mydata.received.line); one record per AADE registration per company (DB constraint)Ληφθέν παραστατικό. Μία εγγραφή για κάθε παραστατικό που εξέδωσε τρίτος στην εταιρεία και καταχώρησε στην ΑΑΔΕ. Είναι δήλωση του εκδότη και όχι δική σας λογιστική εγγραφή: μπορεί να αντιστοιχεί σε τιμολόγιο προμηθευτή που έχετε ήδη, να μην αντιστοιχεί σε τίποτα, ή να ακυρωθεί αργότερα από τον εκδότη. Η σύνδεση με τιμολόγιο (move_id) και η κατάσταση (state) καταγράφουν την απόφαση του λογιστή και φαίνονται στο ιστορικό. Χρησιμοποιήστε το για αναφορές, υπενθυμίσεις ή αυτοματισμούς πάνω σε ό,τι δήλωσαν οι προμηθευτές, για παράδειγμα ανοιχτά παραστατικά χωρίς τιμολόγιο ή παραστατικά ανά προμηθευτή. Τις εγγραφές τις δημιουργεί η σουίτα όταν φέρνει από το myDATA· μην τις δημιουργείτε μόνοι σας και μην αλλάζετε τα στοιχεία καταχώρησης και το αρχικό έγγραφο, γιατί είναι αποδεικτικά. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_mydata_received. Δικαιώματα: οι χρήστες Τιμολόγησης διαβάζουν, δημιουργούν και γράφουν· οι διαχειριστές Λογιστηρίου μπορούν και να διαγράφουν. Το δικό σας module προσθέτει fields ή methods με _inherit = 'l10n_gr.mydata.received.doc'· αν κάνετε override το write, καλέστε πρώτα το super(), γιατί εκεί η σουίτα επανεξετάζει τη μορφή χαρακτηρισμού του συνδεδεμένου τιμολογίου.
Παράδειγμα
Ο οικονομικός διευθυντής θέλει κάθε παραστατικό προμηθευτή που χαρακτηρίζεται ως «αγνοείται» να έχει γραπτή αιτιολογία, ώστε να μην παραλείπεται σιωπηρά τίποτα από όσα δήλωσε ο προμηθευτής.
# Customer need: the finance manager wants a written reason whenever a supplier document is ignored instead of billed.
# depends: ['l10n_gr_nt_mydata_received']
# The model is extended through _inherit; never import from odoo.addons.l10n_gr_*.
from odoo import _, fields, models
from odoo.exceptions import UserError
class ReceivedDoc(models.Model):
_inherit = 'l10n_gr.mydata.received.doc'
x_ignore_reason = fields.Char(string='Reason ignored', tracking=True)
def write(self, vals):
# super() first: the suite re-evaluates the linked bill's classification mode there.
res = super().write(vals)
if vals.get('state') == 'ignored' and self.filtered(lambda d: not d.x_ignore_reason):
# Raising rolls back the whole write, the state included.
raise UserError(_('Write why this document needs no vendor bill before ignoring it.'))
return res
# Add x_ignore_reason to the form with an xpath on the suite's form view.l10n_gr.mydata.received.line
models.Model; key fields: doc_id (Many2one l10n_gr.mydata.received.doc, required, cascade), company_id (related, stored), line_number (Integer), rec_type (Integer, AADE line type), item_description, item_code (Char, often empty on invoices), quantity (Float), net_value, vat_amount, other_taxes_amount, stamp_duty_amount, fees_amount (Monetary), vat_category, vat_exemption_category (Char), issuer_income_category, issuer_income_type (Char, what the issuer declared)Γραμμή ληφθέντος παραστατικού. Οι γραμμές του ληφθέντος παραστατικού, όπως τις έστειλε ο εκδότης. Τα τιμολόγια συνήθως φέρουν μόνο ποσά, χωρίς περιγραφή ή κωδικό είδους, οπότε τα issuer_income_category και issuer_income_type (ο χαρακτηρισμός εσόδου που δήλωσε ο εκδότης) είναι η καλύτερη ένδειξη για το τι αγοράστηκε. Γραμμή με rec_type 6 είναι δωροεπιταγή και δεν χαρακτηρίζεται από τον λήπτη. Χρησιμοποιήστε τη για αναλύσεις ανά κατηγορία ΦΠΑ ή ανά χαρακτηρισμό του εκδότη, ή για δικές σας προτάσεις στην πλευρά των εξόδων. Οι γραμμές διαγράφονται μαζί με το παραστατικό τους. Προϋπόθεση και δικαιώματα: όπως στο l10n_gr.mydata.received.doc.
Παράδειγμα
Πριν από το κλείσιμο του μήνα, ο λογιστής βλέπει τα καθαρά ποσά και τον ΦΠΑ που δήλωσαν οι προμηθευτές σε παραστατικά που δεν έχουν καταχωριστεί ακόμη ως τιμολόγια αγοράς, ανά κατηγορία χαρακτηρισμού του εκδότη και κατηγορία ΦΠΑ.
# Customer need: at month end the accountant sees what still-unbilled supplier documents declare, by income and VAT category.
# depends: ['l10n_gr_nt_mydata_received']
from odoo import api, models
class ReceivedLine(models.Model):
_inherit = 'l10n_gr.mydata.received.line'
@api.model
def _acme_unbilled_summary(self, date_from, date_to):
groups = self._read_group(
[
('company_id', '=', self.env.company.id),
('doc_id.state', '=', 'new'),
('doc_id.issue_date', '>=', date_from),
('doc_id.issue_date', '<=', date_to),
('rec_type', '!=', 6), # gift vouchers are not classified by the recipient
],
['issuer_income_category', 'vat_category'],
['net_value:sum', 'vat_amount:sum'],
)
return [
{'income_category': cat or '-', 'vat_category': vat or '-', 'net': net, 'vat': tax}
for cat, vat, net, tax in groups
]Ελληνικά στοιχεία συναλλασσομένων και εταιρείας
depends: ['l10n_gr_nt_partner']
Μητρώο ΔΟΥ, νομικές μορφές, επωνυμία, αριθμός Γ.Ε.ΜΗ. και κατάσταση VIES σε επαφές και εταιρείες, μαζί με βοηθητικές μεθόδους που επιστρέφουν ό,τι πρέπει να τυπώνουν τα ελληνικά παραστατικά. Απαιτεί το l10n_gr_nt_partner (με το l10n_eu_account_vies).
l10n_gr.doy
_name = 'l10n_gr.doy' (Tax Service); fields: code, name, service_type ('doy' | 'yfe' | 'kefode' | 'kebeis'), doy_class, source_date, active, superseded_by_idΔΟΥ. Το μητρώο των ΔΟΥ και των λοιπών φορολογικών υπηρεσιών, όπως το δημοσιεύει η ΑΑΔΕ. Η ΔΟΥ που καταργήθηκε ή συγχωνεύθηκε αρχειοθετείται, δεν διαγράφεται, και δείχνει στη διάδοχή της μέσω του superseded_by_id, γιατί τα παλαιότερα παραστατικά αναφέρονται σε αυτήν. Διαβάστε και αναζητήστε το μητρώο, και συμπεριλάβετε τις αρχειοθετημένες ΔΟΥ όταν χρειάζεστε το ιστορικό· μη δημιουργείτε ΔΟΥ από ελεύθερο κείμενο.
Παράδειγμα
Μετά από συγχώνευση ΔΟΥ, το back office λαμβάνει εκκρεμότητα για κάθε πελάτη που είναι ακόμη καταχωρισμένος σε καταργημένη ΔΟΥ, ώστε να επιβεβαιώσει τη νέα ΔΟΥ με τον πελάτη.
# Customer need: after an AADE merger, the back office reviews the customers still filed under an abolished tax office.
# depends: ['l10n_gr_nt_partner']
# Offices are read through the ORM (self.env['l10n_gr.doy']); never import from odoo.addons.l10n_gr_*.
from odoo import _, api, models
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def _acme_review_abolished_offices(self):
abolished = self.env['l10n_gr.doy'].with_context(active_test=False).search([
('active', '=', False),
('superseded_by_id', '!=', False),
])
partners = self.search([('l10n_gr_doy_id', 'in', abolished.ids), ('is_company', '=', True)])
for partner in partners:
office = partner.l10n_gr_doy_id
# Do not rewrite the office here: confirm with the customer, then update the contact.
partner.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Tax office %(old)s was merged into %(new)s',
old=office.name,
new=office.l10n_gr_current_service().name), # follows repeated mergers
)
return partnersl10n_gr.doy._l10n_gr_find_by_name
@api.model _l10n_gr_find_by_name(self, text) -> l10n_gr.doy (possibly empty)Εύρεση ΔΟΥ από ονομασία. Επιστρέφει τη ΔΟΥ που ταιριάζει σε μια ονομασία ελεύθερου κειμένου ή κενό recordset. Η σύγκριση αγνοεί πεζά-κεφαλαία, τελείες, αποστρόφους και περιττά κενά. Ελέγχονται και οι αρχειοθετημένες ΔΟΥ, γιατί το κείμενο μπορεί να γράφτηκε πριν από συγχώνευση. Δεν δημιουργεί ποτέ εγγραφή. Χρησιμοποιήστε τη στις εισαγωγές και στις διασυνδέσεις που λαμβάνουν τη ΔΟΥ ως κείμενο, και αφήστε το field κενό όταν δεν βρεθεί.
Παράδειγμα
Κατά τη μετάπτωση από το παλαιό ERP, η ΔΟΥ κάθε πελάτη αντιστοιχίζεται από το ελεύθερο κείμενο του παλαιού συστήματος και οι ονομασίες που δεν αντιστοιχούν σε καμία ΔΟΥ συγκεντρώνονται για χειροκίνητο έλεγχο.
# Customer need: load the customers exported from the legacy ERP, where the tax office is free text such as "Δ.Ο.Υ. Α' ΑΘΗΝΩΝ".
# depends: ['l10n_gr_nt_partner']
# Reached through the ORM (self.env['l10n_gr.doy']); never import from odoo.addons.l10n_gr_*.
from odoo import api, models
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def _acme_import_legacy_customers(self, rows):
"""rows: dicts with afm, name, doy. Returns the office names that were not found."""
Doy = self.env['l10n_gr.doy']
greece = self.env.ref('base.gr')
unmatched = set()
for row in rows:
doy = Doy._l10n_gr_find_by_name(row['doy']) if row.get('doy') else Doy
if row.get('doy') and not doy:
unmatched.add(row['doy']) # leave it empty, never create an office from text
self.create({
'name': row['name'],
'is_company': True,
'country_id': greece.id,
'vat': 'EL%s' % row['afm'],
'l10n_gr_doy_id': doy.id,
})
return sorted(unmatched)l10n_gr.doy.code
fields.Char(string='Code', index=True)Κωδικός TAXIS. Ο τετραψήφιος κωδικός της ΔΟΥ στο TAXIS· τον έχουν μόνο οι ΔΟΥ και κάθε κωδικός ανήκει σε μία ΔΟΥ. Μένει κενός μέχρι να επιβεβαιωθεί, από επίσημο κείμενο ή από τον κωδικό που επιστρέφει το μητρώο της ΑΑΔΕ για έναν ΑΦΜ, γιατί λάθος κωδικός θα τυπωνόταν στα παραστατικά· η ΔΟΥ χωρίς κωδικό λειτουργεί κανονικά σε όλα τα άλλα σημεία. Αναζητήστε με αυτόν όταν τα δεδομένα σας έχουν κωδικό και μη συμπληρώνετε μόνοι σας κωδικούς που λείπουν.
Παράδειγμα
Μια διασύνδεση EDI με αλυσίδα λιανικής λαμβάνει στοιχεία πελατών με τον κωδικό TAXIS της ΔΟΥ και, όταν ο κωδικός λείπει, αναζητά τη ΔΟΥ από την ονομασία της.
# Customer need: the retail chain's EDI customer master sends the TAXIS code of the tax office, sometimes only its name.
# depends: ['l10n_gr_nt_partner']
from odoo import api, models
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def _acme_doy_from_edi(self, code, name):
# Archived offices count: the chain's master data may predate a merger.
Doy = self.env['l10n_gr.doy'].with_context(active_test=False)
code = str(code or '').strip() # the code is a Char; EDI may send it as a number
doy = Doy.search([('code', '=', code)], limit=1) if code else Doy
if not doy and name:
doy = Doy._l10n_gr_find_by_name(name)
return doy
def _acme_apply_edi_master(self, payload):
for partner in self:
doy = self._acme_doy_from_edi(payload.get('TaxOfficeCode'), payload.get('TaxOffice'))
if doy:
# The office belongs to the business: write it on the commercial entity.
partner.commercial_partner_id.l10n_gr_doy_id = doyl10n_gr.doy.l10n_gr_current_service
l10n_gr_current_service(self) -> l10n_gr.doyΗ ΔΟΥ που εξυπηρετεί σήμερα. Ακολουθεί την αλυσίδα των συγχωνεύσεων και επιστρέφει τη ΔΟΥ που εξυπηρετεί σήμερα τον φορολογούμενο· μια ΔΟΥ σε ισχύ επιστρέφει τον εαυτό της. Καλείται σε μία εγγραφή. Χρησιμοποιήστε τη μέθοδο όταν χρειάζεστε την υπηρεσία στην οποία απευθύνεται σήμερα ο φορολογούμενος (αλληλογραφία, υποβολές). Τη ΔΟΥ που είναι αποθηκευμένη στα παραστατικά αφήστε την ως έχει: ένα τιμολόγιο του 2024 πρέπει να δείχνει τη ΔΟΥ που ίσχυε τότε.
Παράδειγμα
Ο λογιστής βλέπει στην επαφή τόσο τη ΔΟΥ που τυπώνεται στα παραστατικά όσο και τη ΔΟΥ που την εξυπηρετεί σήμερα, ώστε η αλληλογραφία να απευθύνεται στη σωστή υπηρεσία.
# Customer need: next to the stored tax office, the accountant sees the office that serves the customer today, for letters and filings.
# depends: ['l10n_gr_nt_partner']
from odoo import api, fields, models
class ResPartner(models.Model):
_inherit = 'res.partner'
x_doy_today_id = fields.Many2one(
'l10n_gr.doy',
string='Tax office serving today',
compute='_compute_x_doy_today_id',
)
@api.depends('l10n_gr_doy_id', 'l10n_gr_doy_id.superseded_by_id')
def _compute_x_doy_today_id(self):
for partner in self:
office = partner.l10n_gr_doy_id
# One record at a time; the stored office stays as printed on past documents.
partner.x_doy_today_id = office.l10n_gr_current_service() if office else Falsel10n_gr.legal.form
_name = 'l10n_gr.legal.form' (Legal Form); fields: code, name, sequence, active, legal_basis, require_gemi, require_legal_form_label, capital_mode, require_guarantee_contributions, require_liquidation_note, require_website, require_exact_address, l10n_gr_verifiedΝομική μορφή. Οι ελληνικές νομικές μορφές (ΑΕ, ΕΠΕ, ΙΚΕ, ΟΕ, ΕΕ, ατομική και λοιπές) και, για καθεμία, ποιες ενδείξεις απαιτεί ο νόμος στα παραστατικά: αριθμός Γ.Ε.ΜΗ., ένδειξη νομικής μορφής, κεφάλαιο, εγγυητικές εισφορές, ένδειξη εκκαθάρισης, ιστοσελίδα, πλήρης διεύθυνση. Το l10n_gr_verified δείχνει αν τον κατάλογο τον έχει επιβεβαιώσει λογιστής. Διαβάστε το για να αποφασίσετε τι τυπώνουν οι δικές σας εκτυπώσεις.
Παράδειγμα
Μια προσαρμοσμένη μορφή προσφοράς τυπώνει τη νομική μορφή, τον αριθμό ΓΕΜΗ και τον ιστότοπο της εταιρείας μόνο όταν τα απαιτεί η νομική της μορφή.
<!-- Customer need: the customer's own quotation layout prints the ΓΕΜΗ number and the legal form only when the company's legal form requires them. -->
<!-- depends: ['sale', 'l10n_gr_nt_partner'] -->
<template id="acme_quotation_company_indications" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[hasclass('page')]" position="inside">
<t t-set="acme_company" t-value="doc.company_id.partner_id"/>
<t t-set="acme_form" t-value="acme_company.l10n_gr_legal_form_id"/>
<div class="small text-muted mt-4">
<span t-out="acme_company.l10n_gr_display_name()"/>
<t t-if="acme_form.require_legal_form_label"> · <span t-out="acme_form.name"/></t>
<t t-if="acme_form.require_gemi and acme_company.l10n_gr_gemi">
· ΓΕΜΗ <span t-out="acme_company.l10n_gr_gemi"/>
</t>
<t t-if="acme_form.require_website and acme_company.website">
· <span t-out="acme_company.website"/>
</t>
</div>
</xpath>
</template>res.company.l10n_gr_aade_vat
l10n_gr_aade_vat(self) -> strΑΦΜ εταιρείας χωρίς πρόθεμα. Ο ΑΦΜ της εκδότριας χωρίς το πρόθεμα χώρας, όπως προκύπτει από την επαφή της, με τον ίδιο κανόνα που εφαρμόζει το res.partner.l10n_gr_aade_vat().
Παράδειγμα
Η επιβεβαίωση παραγγελίας που αποστέλλεται μέσω EDI σε αλυσίδα σούπερ μάρκετ περιλαμβάνει τον ΑΦΜ του πωλητή και του αγοραστή χωρίς πρόθεμα χώρας, όπως τον αναμένει η αλυσίδα.
# Customer need: the supermarket chain's EDI order response must carry both VAT numbers without the EL prefix.
# depends: ['sale', 'l10n_gr_nt_partner']
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
def _acme_edi_order_response(self):
self.ensure_one()
return {
'SenderVat': self.company_id.l10n_gr_aade_vat(),
'ReceiverVat': self.partner_id.commercial_partner_id.l10n_gr_aade_vat(),
'OrderRef': self.client_order_ref or self.name,
'ResponseDate': fields.Date.to_string(fields.Date.context_today(self)),
'Lines': [
{'Ean': line.product_id.barcode, 'Qty': line.product_uom_qty}
for line in self.order_line.filtered(lambda l: not l.display_type)
],
}res.company.l10n_gr_doy_id
fields.Many2one('l10n_gr.doy', string='Tax Office', related='partner_id.l10n_gr_doy_id', readonly=False)ΔΟΥ της εταιρείας. Η ΔΟΥ της εκδότριας, όπως τυπώνεται στα παραστατικά της. Αποθηκεύεται στην επαφή της εταιρείας, οπότε, όποιο από τα δύο κι αν γράψετε, ενημερώνονται και τα δύο.
Παράδειγμα
Μια προσαρμοσμένη αναφορά καρτέλας πελάτη εμφανίζει τον ΑΦΜ και τη ΔΟΥ της εταιρείας ακριβώς όπως εμφανίζονται στα τιμολόγιά της.
<!-- Customer need: the customer's statement of account (a custom report) prints the company's ΑΦΜ and ΔΟΥ as the invoices do. -->
<!-- depends: ['account', 'l10n_gr_nt_partner'] -->
<!-- t-call it from the statement template, where o is the customer record. -->
<template id="acme_statement_company_block">
<t t-set="acme_company" t-value="o.company_id or res_company"/>
<div class="small mb-3">
<strong t-out="acme_company.partner_id.l10n_gr_display_name()"/><br/>
<span t-out="acme_company.partner_id.l10n_gr_full_address()"/><br/>
ΑΦΜ <span t-out="acme_company.l10n_gr_aade_vat()"/>
<t t-if="acme_company.l10n_gr_doy_id">
· ΔΟΥ <span t-out="acme_company.l10n_gr_doy_id.name"/>
</t>
</div>
</template>res.company.l10n_gr_missing_indications
l10n_gr_missing_indications(self) -> list[tuple[str, str]]Ενδείξεις που λείπουν. Ποιες υποχρεωτικές ενδείξεις λείπουν από τα παραστατικά της εκδότριας, ως ζεύγη (ένδειξη, διάταξη). Κενή λίστα σημαίνει πλήρες έντυπο, και είναι πάντα κενή για εταιρεία με φορολογική χώρα εκτός Ελλάδας. Αν λείπει η νομική μορφή, αναφέρεται κι αυτή, γιατί χωρίς αυτήν δεν μπορεί να ελεγχθεί καμία ένδειξη. Καλείται σε μία εταιρεία. Χρησιμοποιήστε τη στους ελέγχους ή στους οδηγούς εγκατάστασης του έργου σας.
Παράδειγμα
Πριν από την έκδοση του πρώτου τιμολογίου, ο σύμβουλος ελέγχει με ένα κουμπί ποιες ενδείξεις που απαιτεί ο νόμος λείπουν ακόμη από τα παραστατικά της εταιρείας.
# Customer need: before go-live, a button in the company form tells the consultant which mandatory indications are still missing.
# depends: ['l10n_gr_nt_partner']
from odoo import _, models
from odoo.exceptions import UserError
class ResCompany(models.Model):
_inherit = 'res.company'
def action_acme_go_live_check(self):
problems = []
for company in self:
problems += [
'%s: %s (%s)' % (company.name, indication, basis)
for indication, basis in company.l10n_gr_missing_indications()
]
if problems:
raise UserError(_('Complete these before the first invoice:\n%s', '\n'.join(problems)))
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {'message': _('All mandatory indications are set.'), 'type': 'success'},
}res.company.l10n_gr_print_capital
l10n_gr_print_capital(self) -> list[tuple[str, float]]Γραμμές κεφαλαίου για εκτύπωση. Τι τυπώνουν τα παραστατικά για το κεφάλαιο της εταιρείας, ως ζεύγη (ετικέτα, ποσό). Κενή λίστα σημαίνει καμία γραμμή κεφαλαίου. Σε νομική μορφή που απαιτεί και καλυφθέν και καταβεβλημένο κεφάλαιο, όπως η ΑΕ, η λίστα είναι σκόπιμα κενή όταν υπάρχει μόνο το ένα από τα δύο ποσά: ο νόμος απαιτεί είτε και τα δύο είτε κανένα. Καλείται σε μία εταιρεία. Χρησιμοποιήστε τη στις δικές σας εκτυπώσεις αντί να διαβάζετε μόνοι σας τα ποσά.
Παράδειγμα
Ένα προσαρμοσμένο υποσέλιδο παραστατικών τυπώνει το κεφάλαιο της εταιρείας μόνο όταν και όπως το απαιτεί η νομική της μορφή, για παράδειγμα και το καλυφθέν και το καταβεβλημένο κεφάλαιο για μια ΑΕ.
<!-- Customer need: the customer's custom document footer prints the share capital only in the form the law accepts for its legal form. -->
<!-- depends: ['web', 'l10n_gr_nt_partner'] -->
<!-- t-call it from your own external layout, where company is set. -->
<template id="acme_footer_capital">
<t t-set="acme_capital" t-value="company.l10n_gr_print_capital()"/>
<div t-if="acme_capital" class="small">
<t t-foreach="acme_capital" t-as="cap">
<span t-out="cap[0]"/>:
<span t-out="cap[1]"
t-options="{'widget': 'monetary', 'display_currency': company.currency_id}"/>
<t t-if="not cap_last"> · </t>
</t>
</div>
</template>res.partner.action_l10n_gr_check_vies
action_l10n_gr_check_vies(self) -> TrueΝέος έλεγχος στο VIES. Ελέγχει ξανά στο VIES, άμεσα, τις επιλεγμένες επαφές. Το Odoo ελέγχει στο VIES μόνο όταν αλλάζει ο ΑΦΜ, οπότε μια επαφή μένει μη επικυρωμένη αν το VIES δεν απαντούσε εκείνη τη στιγμή. Απαιτεί ενεργή την «Επαλήθευση αριθμών ΦΠΑ» στις ρυθμίσεις της Λογιστικής· σηκώνει UserError όταν δεν είναι ενεργή ή όταν κάποια από τις επιλεγμένες επαφές δεν έχει ΑΦΜ.
Παράδειγμα
Κάθε βράδυ ελέγχονται ξανά στο VIES οι επιχειρήσεις-πελάτες της ΕΕ των οποίων ο ΑΦΜ δεν επιβεβαιώθηκε κατά τη δημιουργία τους, ώστε να μην καθυστερούν τα ενδοκοινοτικά τιμολόγια.
# Customer need: EU customers that VIES could not confirm on the day they were created are checked again every night.
# depends: ['l10n_gr_nt_partner']
# Run it from an ir.cron on model res.partner, code: model._cron_acme_recheck_vies()
import logging
from odoo import api, models
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def _cron_acme_recheck_vies(self, limit=50):
eu = self.env.ref('base.europe').country_ids.filtered(lambda c: c.code != 'GR')
candidates = self.search([('is_company', '=', True), ('vat', '!=', False), ('country_id', 'in', eu.ids)])
# l10n_gr_vies_state is computed, not stored: filter in Python.
todo = candidates.filtered(lambda p: p.l10n_gr_vies_state == 'unconfirmed')[:limit]
try:
todo.action_l10n_gr_check_vies()
except UserError as error: # VAT verification disabled in Accounting settings
_logger.warning('VIES re-check skipped: %s', error)res.partner.l10n_gr_aade_vat
l10n_gr_aade_vat(self) -> strΑΦΜ χωρίς πρόθεμα. Ο ΑΦΜ της επαφής χωρίς το πρόθεμα χώρας, με τον ίδιο κανόνα που χρησιμοποιεί παντού η σουίτα, ώστε ο αριθμός που τυπώνετε να είναι ίδιος με αυτόν των παραστατικών. Καλείται σε μία εγγραφή.
Παράδειγμα
Το αρχείο μαζικών πληρωμών προμηθευτών για την τράπεζα περιλαμβάνει τον ΑΦΜ κάθε προμηθευτή χωρίς πρόθεμα χώρας, όπως τον απαιτεί η τράπεζα.
# Customer need: the bank's bulk-payment file needs each supplier's ΑΦΜ without the EL prefix.
# depends: ['account', 'l10n_gr_nt_partner']
import csv
import io
from odoo import models
class AccountPayment(models.Model):
_inherit = 'account.payment'
def _acme_bank_file(self):
out = io.StringIO()
writer = csv.writer(out, delimiter=';')
for pay in self.filtered(lambda p: p.payment_type == 'outbound'):
supplier = pay.partner_id.commercial_partner_id
writer.writerow([
supplier.l10n_gr_aade_vat(),
supplier.l10n_gr_display_name()[:70],
pay.partner_bank_id.acc_number or '',
'%.2f' % pay.amount,
pay.memo or pay.name,
])
return out.getvalue()res.partner.l10n_gr_display_name
l10n_gr_display_name(self) -> strΕπωνυμία για εκτύπωση. Η επωνυμία που τυπώνει το παραστατικό για την επαφή: η επίσημη επωνυμία όταν υπάρχει, αλλιώς το όνομα της επαφής. Καλείται σε μία εγγραφή. Χρησιμοποιήστε τη στις δικές σας εκτυπώσεις, ώστε να συμφωνούν με τα παραστατικά της σουίτας.
Παράδειγμα
Η ετικέτα courier που τυπώνεται από κάθε παράδοση εμφανίζει την επωνυμία του πελάτη, την ίδια που φέρουν τα τιμολόγιά του.
<!-- Customer need: the courier label printed from each delivery shows the customer's registered name, not the trade name. -->
<!-- depends: ['stock', 'l10n_gr_nt_partner'] -->
<record id="acme_report_courier_label" model="ir.actions.report">
<field name="name">Courier label</field>
<field name="model">stock.picking</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">acme_custom.courier_label</field>
<field name="binding_model_id" ref="stock.model_stock_picking"/>
</record>
<template id="courier_label">
<t t-call="web.basic_layout">
<div t-foreach="docs" t-as="o" class="page">
<t t-set="customer" t-value="o.partner_id.commercial_partner_id"/>
<h3 t-out="customer.l10n_gr_display_name()"/>
<div t-out="o.partner_id.l10n_gr_full_address()"/>
<div>ΑΦΜ <span t-out="customer.l10n_gr_aade_vat()"/></div>
<div t-out="o.name"/>
</div>
</t>
</template>res.partner.l10n_gr_doy_id
fields.Many2one('l10n_gr.doy', string='Tax Office', ondelete='restrict', index=True)ΔΟΥ της επαφής. Η ΔΟΥ στην οποία υπάγεται η επαφή· τυπώνεται στα παραστατικά. Ανήκει στην επιχείρηση και όχι στη διεύθυνση: κρατιέται στην εμπορική οντότητα και οι διευθύνσεις παράδοσης και τιμολόγησης την ακολουθούν, οπότε γράψτε τη στην επαφή της εταιρείας. Συμπληρώστε τη από τις εισαγωγές σας ή από τον συγχρονισμό με CRM. ΔΟΥ που χρησιμοποιείται δεν διαγράφεται.
Παράδειγμα
Μια παραγγελία πώλησης προς ελληνική επιχείρηση δεν επιβεβαιώνεται πριν συμπληρωθεί η ΔΟΥ του πελάτη, ώστε το τιμολόγιο να μην εκδίδεται ποτέ χωρίς αυτήν.
# Customer need: a sale order for a Greek business is not confirmed until its tax office is set, because the invoice must print it.
# depends: ['sale', 'l10n_gr_nt_partner']
from odoo import _, models
from odoo.exceptions import UserError
class SaleOrder(models.Model):
_inherit = 'sale.order'
def action_confirm(self):
for order in self:
# The office belongs to the business, so read it on the commercial entity.
customer = order.partner_invoice_id.commercial_partner_id
if (customer.is_company and customer.country_id.code == 'GR'
and customer.vat and not customer.l10n_gr_doy_id):
raise UserError(_(
'Set the tax office of %s before confirming: the invoice must print it.',
customer.display_name,
))
return super().action_confirm()res.partner.l10n_gr_full_address
l10n_gr_full_address(self) -> strΔιεύθυνση σε μία γραμμή. Η έδρα σε μία γραμμή: οδός με αριθμό, ΤΚ, πόλη. Καλείται σε μία εγγραφή. Χρησιμοποιήστε τη όπου η διεύθυνση πρέπει να διαβάζεται ως μία ένδειξη, κάτι που δεν προσφέρει το widget contact του Odoo, αφού τη δείχνει σε πολλές γραμμές.
Παράδειγμα
Το υποσέλιδο του ηλεκτρονικού καταστήματος του πελάτη εμφανίζει την επωνυμία, την έδρα σε μία γραμμή, τον ΑΦΜ, τη ΔΟΥ και τον αριθμό ΓΕΜΗ της εταιρείας.
<!-- Customer need: the e-shop footer shows the seller's legal identity, with the registered address on one line. -->
<!-- depends: ['website', 'l10n_gr_nt_partner'] -->
<template id="acme_website_legal_footer" inherit_id="website.layout">
<xpath expr="//footer" position="inside">
<t t-set="acme_seller" t-value="res_company.partner_id"/>
<div class="container small text-center py-2">
<span t-out="acme_seller.l10n_gr_display_name()"/> ·
<span t-out="acme_seller.l10n_gr_full_address()"/> ·
ΑΦΜ <span t-out="res_company.l10n_gr_aade_vat()"/>
<t t-if="res_company.l10n_gr_doy_id">
· ΔΟΥ <span t-out="res_company.l10n_gr_doy_id.name"/>
</t>
<t t-if="acme_seller.l10n_gr_gemi">
· ΓΕΜΗ <span t-out="acme_seller.l10n_gr_gemi"/>
</t>
</div>
</xpath>
</template>res.partner.l10n_gr_full_street
l10n_gr_full_street(self) -> strΟδός με αριθμό. Η οδός μαζί με τον αριθμό, όταν ο αριθμός βρίσκεται σε χωριστό field· αν η οδός περιέχει ήδη τον αριθμό, δεν τον επαναλαμβάνει. Καλείται σε μία εγγραφή.
Παράδειγμα
Η διασύνδεση με εταιρεία courier αποστέλλει για κάθε παράδοση την οδό μαζί με τον αριθμό ως ενιαία τιμή, χωρίς να επαναλαμβάνει αριθμό που ήδη περιέχεται στην οδό.
# Customer need: the courier's API wants street with number, postcode and city as separate values.
# depends: ['stock', 'l10n_gr_nt_partner']
from odoo import _, models
from odoo.exceptions import UserError
class StockPicking(models.Model):
_inherit = 'stock.picking'
def _acme_courier_payload(self):
self.ensure_one()
recipient = self.partner_id
if not (recipient.street and recipient.zip and recipient.city):
raise UserError(_('The delivery address of %s is incomplete.', self.name))
return {
'name': recipient.commercial_partner_id.l10n_gr_display_name(),
'street': recipient.l10n_gr_full_street(), # the number is added once, never twice
'postcode': recipient.zip,
'city': recipient.city,
'phone': recipient.phone or '',
'reference': self.name,
}res.partner.l10n_gr_gemi
fields.Char(string='Commercial Registry Number')Αριθμός Γ.Ε.ΜΗ.. Ο αριθμός Γενικού Εμπορικού Μητρώου της επαφής: ακριβώς δώδεκα ψηφία και μοναδικός ανά επιχείρηση (η εταιρεία και τα υποκαταστήματά της τον μοιράζονται). Αριθμός με άλλο πλήθος ψηφίων ή αριθμός που ανήκει ήδη σε άλλη επιχείρηση σηκώνει σφάλμα επικύρωσης, οπότε καθαρίστε την τιμή στις εισαγωγές σας πριν τη γράψετε. Αν πρέπει να τυπωθεί εξαρτάται από τη νομική μορφή.
Παράδειγμα
Ο συγχρονισμός με το CRM αποθηκεύει τον αριθμό ΓΕΜΗ κάθε πελάτη αφού τον καθαρίσει, και καταγράφει σημείωση στην επαφή όταν ο αριθμός ανήκει ήδη σε άλλη επιχείρηση.
# Customer need: the CRM sync writes the ΓΕΜΗ number it receives, cleaned, and reports duplicates instead of stopping the sync.
# depends: ['l10n_gr_nt_partner']
import re
from odoo import _, models
from odoo.exceptions import ValidationError
class ResPartner(models.Model):
_inherit = 'res.partner'
def _acme_sync_gemi(self, raw):
self.ensure_one()
digits = re.sub(r'\D', '', raw or '')
if len(digits) != 12:
return False
business = self.commercial_partner_id # a company and its branches share the number
try:
with self.env.cr.savepoint():
business.l10n_gr_gemi = digits
except ValidationError as error: # already used by another business
business.message_post(body=_('ΓΕΜΗ %(gemi)s not saved: %(error)s', gemi=digits, error=error))
return False
return Trueres.partner.l10n_gr_legal_form_id
fields.Many2one('l10n_gr.legal.form', string='Legal Form', ondelete='restrict')Νομική μορφή της επαφής. Η νομική μορφή της επαφής, που ορίζει ποιες ενδείξεις τυπώνονται στα παραστατικά. Στην επαφή της ίδιας της εταιρείας καθορίζει τον έλεγχο των ενδείξεων που λείπουν.
Παράδειγμα
Κατά τη μετάπτωση από παλαιό ERP, η νομική μορφή κάθε πελάτη, που εκεί τηρείται ως κείμενο, ορίζεται στην επαφή ώστε τα παραστατικά να τυπώνουν τις σωστές ενδείξεις.
# Customer need: the legacy ERP stores the legal form as text (ΑΕ, Ε.Π.Ε., ΙΚΕ, ΟΕ, ΕΕ, Ατομική); the migration maps it to the Greek legal forms.
# depends: ['l10n_gr_nt_partner']
from odoo import api, models
# Keys are the legacy text without dots or spaces, in capitals.
LEGACY_FORMS = {
'ΑΕ': 'ae', 'ΕΠΕ': 'epe', 'ΙΚΕ': 'ike', 'ΟΕ': 'oe', 'ΕΕ': 'ee',
'ΑΤΟΜΙΚΗ': 'individual', 'ΑΤΟΜΙΚΉ': 'individual',
}
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def _acme_migrate_legal_forms(self, rows):
"""rows: (afm, legacy legal form text) pairs from the legacy export."""
forms = {f.code: f for f in self.env['l10n_gr.legal.form'].search([])}
for afm, legacy in rows:
key = ''.join((legacy or '').replace('.', '').split()).upper()
code = LEGACY_FORMS.get(key)
partner = self.search([('vat', 'in', [afm, 'EL' + afm]), ('is_company', '=', True)], limit=1)
if partner and code in forms:
partner.l10n_gr_legal_form_id = forms[code]res.partner.l10n_gr_legal_name
fields.Char(string='Legal Name')Επωνυμία. Η επωνυμία όπως είναι καταχωρημένη στο Γ.Ε.ΜΗ. Τα παραστατικά την τυπώνουν αντί για τον διακριτικό τίτλο· όταν λείπει, τυπώνεται το όνομα της επαφής. Διαβάστε τη μέσω του l10n_gr_display_name() και όχι απευθείας, ώστε οι εκτυπώσεις σας να ακολουθούν τον ίδιο κανόνα.
Παράδειγμα
Κατά τη μετάπτωση, κάθε πελάτης διατηρεί τον διακριτικό τίτλο ως όνομα επαφής και την επωνυμία του για τα παραστατικά, όπως τα τηρούσε το παλαιό ERP.
# Customer need: the legacy ERP keeps «επωνυμία» and «διακριτικός τίτλος» apart; Odoo keeps the trade name as name, the registered name for documents.
# depends: ['l10n_gr_nt_partner']
from odoo import api, models
class ResPartner(models.Model):
_inherit = 'res.partner'
@api.model
def _acme_migrate_names(self, rows):
"""rows: dicts with afm, eponymia, titlos from the legacy export."""
for row in rows:
afm = row['afm']
partner = self.search([('vat', 'in', [afm, 'EL' + afm]), ('is_company', '=', True)], limit=1)
if not partner:
continue
legal = (row.get('eponymia') or '').strip()
trade = (row.get('titlos') or '').strip()
partner.write({
'name': trade or legal or partner.name,
# Empty when it equals the trade name: documents then print the name.
'l10n_gr_legal_name': legal if legal and legal != trade else False,
})res.partner.l10n_gr_vies_state
fields.Selection([('no_vat', ...), ('domestic', ...), ('disabled', ...), ('valid', ...), ('unconfirmed', ...)], string='VIES status', compute='_compute_l10n_gr_vies_state') (not stored)Κατάσταση VIES. Η κατάσταση της επαφής στο VIES σε απλή μορφή: χωρίς ΑΦΜ, επικυρωμένος, ελληνική επαφή (το VIES αφορά μόνο τις ενδοκοινοτικές συναλλαγές), επικύρωση VIES ανενεργή ή μη επικυρωμένος. Είναι computed και όχι stored: διαβάστε το σε εγγραφές, για παράδειγμα πριν από ενδοκοινοτικό τιμολόγιο.
Παράδειγμα
Ένα τιμολόγιο χωρίς ΦΠΑ προς επιχείρηση της ΕΕ καταχωρίζεται μόνο αφού το VIES επικυρώσει τον ΑΦΜ του πελάτη, με έναν νέο έλεγχο αν δεν είχε επιβεβαιωθεί προηγουμένως.
# Customer need: an intra-community invoice without VAT is not posted for a customer that VIES has not validated.
# depends: ['account', 'l10n_gr_nt_partner']
from odoo import _, models
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
def action_post(self):
eu = self.env.ref('base.europe').country_ids
for move in self.filtered(lambda m: m.move_type == 'out_invoice'):
customer = move.commercial_partner_id
zero_vat = not move.invoice_line_ids.tax_ids.filtered(lambda t: t.amount)
if customer.country_id in eu and customer.country_id.code != 'GR' and zero_vat:
if customer.l10n_gr_vies_state == 'unconfirmed':
customer.action_l10n_gr_check_vies() # VIES may have been down that day
if customer.l10n_gr_vies_state != 'valid':
raise UserError(_(
'%s is not validated in VIES: an invoice without VAT needs a valid EU VAT number.',
customer.display_name,
))
return super().action_post()Ελληνικά Έντυπα
depends: ['l10n_gr_nt_report_layout']
Τυπώνει στα παραστατικά τα στοιχεία που απαιτεί η ελληνική νομοθεσία, όπως ΑΦΜ, ΔΟΥ, ΓΕΜΗ και τραπεζικούς λογαριασμούς.
account.move._l10n_gr_document_title
_l10n_gr_document_title(self) -> str or False; single record (ensure_one)Τίτλος εντύπου. Ο τίτλος που τυπώνεται στην κορυφή του εντύπου τιμολογίου και που, κατά τον νόμο, πρέπει να λέει τι είναι το παραστατικό, για παράδειγμα «Τιμολόγιο Πώλησης». Επιστρέφει την ονομασία του τύπου ΑΑΔΕ, με τη διατύπωση δελτίου αποστολής όταν η σειρά είναι σειρά δελτίου. Επιστρέφει False, δηλαδή κρατά τον τίτλο του Odoo, όταν η κίνηση δεν είναι καταχωρημένη, η χώρα φορολογίας της εταιρείας δεν είναι η Ελλάδα, ή δεν βρίσκεται τύπος ΑΑΔΕ. Κάντε override για να προσαρμόσετε τη διατύπωση στο έντυπο ενός πελάτη· καλέστε super() και αλλάξτε μόνο αποτέλεσμα που δεν είναι κενό, ώστε τα πρόχειρα και οι μη ελληνικές εταιρείες να κρατούν τον τίτλο του Odoo. Το επεκτείνουν και άλλα modules της σουίτας, οπότε καλείτε πάντα το super(). Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_report_layout.
Παράδειγμα
Μια εξαγωγική εταιρεία τυπώνει τον νόμιμο ελληνικό τίτλο κάθε τιμολογίου μαζί με την αγγλική ονομασία του, όταν ο πελάτης δεν διαβάζει ελληνικά.
# Customer need: an exporter wants the printed title in Greek with the English name next to it for foreign customers.
# depends: ['l10n_gr_nt_report_layout']
# Extend through _inherit and super(); never import from odoo.addons.l10n_gr_*.
from odoo import models
# By AADE document type, so a retail receipt is never called an invoice.
ENGLISH_TITLES = {
'1.1': 'Sales Invoice', '1.2': 'Sales Invoice, Intra-Community Supply',
'1.3': 'Sales Invoice, Export', '2.1': 'Service Invoice',
'2.2': 'Service Invoice, Intra-Community', '2.3': 'Service Invoice, Third Country',
'5.1': 'Credit Note',
}
class AccountMove(models.Model):
_inherit = 'account.move'
def _l10n_gr_document_title(self):
title = super()._l10n_gr_document_title()
# False means keep Odoo's own title (drafts, non-Greek companies): leave it alone.
if not title:
return title
english = ENGLISH_TITLES.get(self._l10n_gr_invoice_type().code)
if english and not (self.partner_id.lang or '').startswith('el'):
return '%s / %s' % (title, english)
return titleaccount.move._l10n_gr_invoice_type
_l10n_gr_invoice_type(self) -> aade.invoice.type record (possibly empty) or False; single record (ensure_one)Τύπος παραστατικού ΑΑΔΕ. Επιστρέφει τον τύπο παραστατικού ΑΑΔΕ μιας κίνησης: πρώτα από τη σειρά του παραστατικού, γιατί αυτός είναι που καταχωρήθηκε, μετά τον τύπο πιστωτικού του ημερολογίου για τα πιστωτικά, και τέλος τον τύπο που ορίζει το ημερολόγιο. Όταν δεν ισχύει κανένα, για παράδειγμα σε ημερολόγιο χωρίς ελληνική παραμετροποίηση, επιστρέφει κενή εγγραφή ή False, οπότε ελέγχετε αν το αποτέλεσμα είναι κενό και δεν το συγκρίνετε με recordset. Χρησιμοποιήστε τη σε δικές σας αναφορές, ελέγχους ή εξαγωγές όταν χρειάζεται να ξέρετε τι είδους ελληνικό παραστατικό είναι η κίνηση, αντί να το συμπεραίνετε από το move_type. Καλείται σε μία εγγραφή. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_report_layout.
Παράδειγμα
Οι χρήστες δεν μπορούν να καταχωρίσουν απόδειξη λιανικής για επιχείρηση-πελάτη με ΑΦΜ, ώστε οι πελάτες αυτοί να λαμβάνουν πάντα τιμολόγιο.
# Customer need: a retail receipt must not be posted for a business customer with a VAT number; staff must use an invoice series.
# depends: ['l10n_gr_nt_report_layout']
from odoo import _, models
from odoo.exceptions import UserError
class AccountMove(models.Model):
_inherit = 'account.move'
def action_post(self):
for move in self.filtered(lambda m: m.move_type == 'out_invoice'):
kind = move._l10n_gr_invoice_type()
customer = move.commercial_partner_id
# Test for truth: the method returns an empty record or False when nothing applies.
if kind and kind.code.startswith('11.') and customer.is_company and customer.vat:
raise UserError(_(
'%(doc)s is a retail document (%(kind)s) but %(customer)s is a business: use an invoice series.',
doc=move.display_name,
kind=kind.display_name,
customer=customer.display_name,
))
return super().action_post()Αυτοπαράδοση και Ιδιοχρησιμοποίηση
depends: ['l10n_gr_nt_selfsupply']
Καταγράφει την αυτοπαράδοση αγαθών και την ιδιοχρησιμοποίηση υπηρεσιών, συνδέοντας την κίνηση αποθήκης με το παραστατικό.
account.move._l10n_gr_self_supply_source_location
_l10n_gr_self_supply_source_location(self) -> stock.location; single record (ensure_one)Από πού φεύγουν τα αγαθά της αυτοπαράδοσης. Επιστρέφει την τοποθεσία αποθήκης από την οποία φεύγουν τα αγαθά ενός στοιχείου αυτοπαράδοσης. Από προεπιλογή είναι η τοποθεσία αποθέματος της πρώτης αποθήκης της εταιρείας. Η σουίτα την καλεί και στον έλεγχο αποθέματος πριν την καταχώρηση και στη δημιουργία των κινήσεων, οπότε ένα override αλλάζει και τα δύο με συνέπεια. Κάντε override όταν ο πελάτης έχει περισσότερες αποθήκες και τα αγαθά πρέπει να φεύγουν από συγκεκριμένη· επιστρέψτε μία stock.location της εταιρείας της κίνησης ή, στις υπόλοιπες περιπτώσεις, το αποτέλεσμα του super(). Κενή τοποθεσία σταματά την καταχώρηση με σφάλμα. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_selfsupply, με ορισμένη στις ρυθμίσεις της εταιρείας την τοποθεσία αυτοπαράδοσης.
Παράδειγμα
Μια εταιρεία με κεντρική αποθήκη και πολλά καταστήματα θέλει τα αγαθά της αυτοπαράδοσης να εξέρχονται πάντα από την αποθήκη που έχει επιλέξει στις ρυθμίσεις της.
# Customer need: the company has a central warehouse and shops; goods for own use always leave from the warehouse set on the company.
# depends: ['stock', 'l10n_gr_nt_selfsupply']
from odoo import fields, models
class ResCompany(models.Model):
_inherit = 'res.company'
x_self_supply_warehouse_id = fields.Many2one('stock.warehouse', string='Self-supply warehouse')
class AccountMove(models.Model):
_inherit = 'account.move'
def _l10n_gr_self_supply_source_location(self):
# The suite uses this for the stock check and for the moves, so both follow.
warehouse = self.company_id.x_self_supply_warehouse_id
if warehouse and warehouse.company_id == self.company_id:
return warehouse.lot_stock_id
return super()._l10n_gr_self_supply_source_location()account.move.l10n_gr_self_supply_entry_id
fields.Many2one('account.move'), copy=False, ondelete='set null'Εγγραφή τακτοποίησης. Στο Odoo κάθε στοιχείο αυτοπαράδοσης έχει υποχρεωτικά μια απαίτηση· αυτή η λογιστική εγγραφή την κλείνει, ώστε η καρτέλα της ίδιας της εταιρείας να μη δείχνει ανοιχτό υπόλοιπο. Η σουίτα την καταχωρεί και τη συμψηφίζει όταν καταχωρείται το παραστατικό. Χρησιμοποιήστε το για να ανοίγετε ή να εμφανίζετε την τακτοποίηση δίπλα στο παραστατικό. Μόνο για ανάγνωση: η σουίτα παραλείπει την τακτοποίηση όταν το field είναι ήδη συμπληρωμένο. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_selfsupply, με ορισμένα στις ρυθμίσεις της εταιρείας το ημερολόγιο και τον λογαριασμό εξόδου της αυτοπαράδοσης.
Παράδειγμα
Ο λογιστής λαμβάνει υπενθύμιση για κάθε καταχωρισμένο παραστατικό αυτοπαράδοσης που δεν έχει εγγραφή συμψηφισμού, ώστε να μη μένει ανοιχτό υπόλοιπο στον λογαριασμό της ίδιας της εταιρείας.
# Customer need: the accountant is reminded of posted self-supply documents left without a settlement entry, for example after it was deleted.
# depends: ['l10n_gr_nt_selfsupply']
# Run it from an ir.cron on model account.move, code: model._cron_acme_unsettled_self_supply()
from odoo import _, api, models
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def _cron_acme_unsettled_self_supply(self):
# Read only: the suite posts the settlement itself, never set the field from here.
unsettled = self.search([
('l10n_gr_self_supply_type', '!=', False),
('state', '=', 'posted'),
('l10n_gr_self_supply_entry_id', '=', False),
])
for doc in unsettled.filtered(lambda d: not d.activity_ids):
doc.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Self-supply without settlement entry'),
)account.move.l10n_gr_self_supply_move_ids
fields.One2many('stock.move', 'l10n_gr_self_supply_invoice_id')Κινήσεις αποθήκης της αυτοπαράδοσης. Οι κινήσεις αποθήκης που δημιούργησε ένα στοιχείο αυτοπαράδοσης αγαθών κατά την καταχώρησή του, μία για κάθε γραμμή αποθηκεύσιμου είδους. Οι υπηρεσίες και τα μη αποθηκεύσιμα είδη δεν δημιουργούν κίνηση. Χρησιμοποιήστε το για να βρίσκετε ποιο απόθεμα εξήλθε με ένα παραστατικό, για παράδειγμα σε αναφορά ή έλεγχο. Το διαβάζετε· μην προσθέτετε κινήσεις μόνοι σας, γιατί η σουίτα δημιουργεί κινήσεις μόνο σε παραστατικό που δεν έχει ακόμη. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_selfsupply.
Παράδειγμα
Η αποθήκη τυπώνει για κάθε παραστατικό αυτοπαράδοσης ένα φύλλο με τα είδη, τις θέσεις και τις ποσότητες που εξήλθαν από το απόθεμα, προς υπογραφή.
<!-- Customer need: the warehouse keeps a signed sheet of what each self-supply document took out of stock. -->
<!-- depends: ['stock', 'l10n_gr_nt_selfsupply'] -->
<!-- Bind it with an ir.actions.report on account.move (report_name acme_custom.self_supply_stock_sheet). -->
<template id="self_supply_stock_sheet">
<t t-call="web.html_container">
<t t-foreach="docs" t-as="o">
<t t-call="web.external_layout">
<div class="page">
<h4><span t-out="o.name"/> · <span t-out="o.invoice_date"/></h4>
<table class="table table-sm">
<tr t-foreach="o.l10n_gr_self_supply_move_ids" t-as="m">
<td t-out="m.product_id.display_name"/>
<td t-out="m.location_id.display_name"/>
<td class="text-end"><span t-out="m.quantity"/> <span t-out="m.product_uom.name"/></td>
</tr>
</table>
<p t-if="not o.l10n_gr_self_supply_move_ids">No stock moves: services or non-storable products only.</p>
<p class="mt-5">Signature: ____________________</p>
</div>
</t>
</t>
</t>
</template>account.move.l10n_gr_self_supply_type
fields.Char, computed from the document series type, stored, indexed; '6.1' (self-supply of goods), '6.2' (own use of services) or FalseΤύπος αυτοπαράδοσης. Δείχνει αν η κίνηση είναι στοιχείο αυτοπαράδοσης: '6.1' για αγαθά που φεύγουν από το απόθεμα, '6.2' για υπηρεσίες που χρησιμοποιεί η ίδια η επιχείρηση, κενό για όλα τα άλλα. Προκύπτει από τη σειρά του παραστατικού, οπότε δεν το γράφετε απευθείας· ορίζετε τη σειρά. Είναι αποθηκευμένο, άρα μπαίνει σε domain, φίλτρα και ομαδοποίηση, για παράδειγμα για να μένουν τα στοιχεία αυτοπαράδοσης έξω από αναφορές πωλήσεων ή για να τα βλέπει συγκεντρωμένα ο λογιστής. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_selfsupply.
Παράδειγμα
Ο μηνιαίος πίνακας του ιδιοκτήτη εμφανίζει τις πωλήσεις χωριστά από την αυτοπαράδοση αγαθών και την ιδιοχρησιμοποίηση υπηρεσιών, ώστε ο κύκλος εργασιών να μην εμφανίζεται διογκωμένος.
# Customer need: the owner's monthly dashboard shows self-supplies apart from sales, so own use never inflates turnover.
# depends: ['l10n_gr_nt_selfsupply']
from odoo import api, models
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def _acme_turnover_and_self_supply(self, date_from, date_to):
domain = [
('company_id', '=', self.env.company.id),
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('invoice_date', '>=', date_from),
('invoice_date', '<=', date_to),
]
# Stored and indexed, so it groups in SQL; False means an ordinary sale.
rows = self._read_group(
domain,
['l10n_gr_self_supply_type', 'invoice_date:month'],
['amount_untaxed_signed:sum'],
)
return [{'kind': kind or 'sales', 'month': month, 'net': net} for kind, month, net in rows]stock.move.l10n_gr_self_supply_invoice_id
fields.Many2one('account.move'), copy=False, ondelete='set null', indexedΣτοιχείο αυτοπαράδοσης της κίνησης. Σε μια κίνηση αποθήκης, το στοιχείο αυτοπαράδοσης που τη δημιούργησε· κενό σε κάθε άλλη κίνηση. Είναι η σύνδεση από την πλευρά της αποθήκης και απαντά στο ερώτημα γιατί βγήκε απόθεμα χωρίς παράδοση. Χρησιμοποιήστε το σε αναφορές και φίλτρα αποθήκης, για παράδειγμα για να ξεχωρίζετε την αυτοπαράδοση από την καταστροφή ή τις πωλήσεις. Το διαβάζετε· το συμπληρώνει η σουίτα όταν δημιουργεί την κίνηση. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_selfsupply.
Παράδειγμα
Η μηνιαία αναφορά του υπευθύνου αποθήκης δείχνει ανά είδος πόσο απόθεμα εξήλθε για αυτοπαράδοση, πόσο καταστράφηκε και πόσο πωλήθηκε.
# Customer need: the warehouse manager's monthly outflow report separates goods taken for own use from sales and scrap.
# depends: ['sale_stock', 'l10n_gr_nt_selfsupply']
from odoo import api, models
class StockMove(models.Model):
_inherit = 'stock.move'
@api.model
def _acme_outflow_by_reason(self, date_from, date_to):
base = [
('state', '=', 'done'), ('date', '>=', date_from), ('date', '<=', date_to),
('location_id.usage', '=', 'internal'), ('location_dest_id.usage', '!=', 'internal'),
]
reasons = {
'self_supply': [('l10n_gr_self_supply_invoice_id', '!=', False)],
'scrap': [('l10n_gr_self_supply_invoice_id', '=', False), ('scrap_id', '!=', False)],
'sale': [('l10n_gr_self_supply_invoice_id', '=', False), ('sale_line_id', '!=', False)],
}
return {
reason: dict(self._read_group(base + extra, ['product_id'], ['product_qty:sum']))
for reason, extra in reasons.items()
}Ψηφιακό Δελτίο Αποστολής
depends: ['l10n_gr_nt_stock']
Εκδίδει το δελτίο αποστολής από τη διακίνηση, με τα στοιχεία μεταφοράς που χρειάζεται το παραστατικό (σκοπός διακίνησης, όχημα, ημερομηνία αποστολής), και συνδέει κάθε τιμολόγιο πώλησης με τα δελτία που δηλώνει.
account.move.l10n_gr_connected_picking_ids
fields.Many2many('stock.picking', 'account_move_l10n_gr_connected_picking_rel', 'move_id', 'picking_id', string='Connected Delivery Notes', readonly=True, copy=False)Συνδεδεμένα δελτία αποστολής τιμολογίου. Τα δελτία αποστολής που δηλώνει ένα τιμολόγιο πώλησης ως συσχετιζόμενα παραστατικά. Με την καταχώρηση του τιμολογίου, η σουίτα το συμπληρώνει (αν είναι ακόμη κενό) με τα ολοκληρωμένα και ήδη διαβιβασμένα δελτία παράδοσης των γραμμών παραγγελίας που τιμολογεί αυτό το τιμολόγιο, όχι με όλες τις παραδόσεις της παραγγελίας, και αφήνει έξω όσα έχει ήδη δηλώσει άλλο καταχωρημένο τιμολόγιο. Από εκεί και πέρα η λίστα δεν αλλάζει, ώστε το τιμολόγιο που τυπώθηκε και αυτό που δηλώθηκε να συμφωνούν. Χρησιμοποιήστε το για να βρείτε, από ένα τιμολόγιο, ποιες παραδόσεις καλύπτει (αναφορές, αυτοματισμοί, έλεγχοι στο δικό σας module). Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_stock. Το field είναι μόνο για ανάγνωση: το περιεχόμενό του το ορίζει η σουίτα, και μια αλλαγή μετά την καταχώρηση θα έκανε το τιμολόγιο να διαφέρει από ό,τι δηλώθηκε.
Παράδειγμα
Όταν καταχωρίζεται ένα τιμολόγιο, οι παραδόσεις που δηλώνει σημειώνονται με τον αριθμό του, ώστε το σύστημα διαχείρισης αποθήκης να τις κλείσει.
# Customer need: the WMS must know which deliveries an invoice covers, so it can close them on its side.
# depends: ['l10n_gr_nt_stock']
from odoo import fields, models
class StockPicking(models.Model):
_inherit = 'stock.picking'
x_wms_invoice_ref = fields.Char(string='Invoiced by', copy=False, readonly=True)
class AccountMove(models.Model):
_inherit = 'account.move'
def _post(self, soft=True):
posted = super()._post(soft=soft)
# Read after super(): the suite fills the list while posting, then keeps it fixed.
for move in posted.filtered(lambda m: m.move_type == 'out_invoice'):
move.l10n_gr_connected_picking_ids.write({'x_wms_invoice_ref': move.name})
return postedstock.picking.dispatch_date
fields.Datetime(string='Dispatch Date', copy=False)Ημερομηνία και ώρα αποστολής. Η προγραμματισμένη ή εκτιμώμενη ώρα αναχώρησης της αποστολής. Τυπώνεται στο δελτίο αποστολής και περιλαμβάνεται στα στοιχεία του. Κατά την έκδοση η σουίτα δεν δέχεται ημέρα αποστολής στο παρελθόν, και αν το field είναι κενό συμπληρώνει τη στιγμή της έκδοσης. Όταν τη διακίνηση την καλύπτει τιμολόγιο που είναι και δελτίο αποστολής (σειρά τιμολογίου συνδυασμένη με σειρά δελτίου), το τιμολόγιο παίρνει την ημερομηνία αποστολής από εδώ, αλλιώς από την ημερομηνία ολοκλήρωσης της διακίνησης. Ορίστε το από module που προγραμματίζει αποστολές, ώστε το παραστατικό να φέρει την προγραμματισμένη αναχώρηση. Δεν αντιγράφεται. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_stock.
Παράδειγμα
Ο υπεύθυνος δρομολογίων ορίζει το φορτηγό και την ώρα αναχώρησης στις παραδόσεις της επόμενης ημέρας, ώστε κάθε δελτίο αποστολής να τα αναγράφει χωρίς χειροκίνητη καταχώριση.
# Customer need: the route planner sets the truck and its departure time on tomorrow's deliveries, so the delivery note carries them.
# depends: ['l10n_gr_nt_stock']
from datetime import timedelta
from odoo import _, fields, models
from odoo.exceptions import UserError
class StockPicking(models.Model):
_inherit = 'stock.picking'
def action_acme_plan_route(self, plate, departure=None):
"""plate: the truck's registration plate; departure: datetime, default in one hour."""
departure = departure or fields.Datetime.now() + timedelta(hours=1)
if departure < fields.Datetime.now():
# The suite itself refuses a dispatch day in the past when the note is issued.
raise UserError(_('The departure time is in the past.'))
# Only deliveries still to ship.
todo = self.filtered(lambda p: p.state not in ('done', 'cancel'))
todo.write({
'vehicle_number': plate,
'dispatch_date': departure,
})
return Truestock.picking.move_purpose_id
fields.Many2one('aade.move.purpose', string='Movement Purpose', compute=..., store=True, readonly=False, precompute=True, domain="[('id', 'in', allowed_move_purpose_ids)]", copy=False)Σκοπός διακίνησης. Ο λόγος για τον οποίο διακινούνται τα είδη (πίνακας 8.14 της ΑΑΔΕ). Τυπώνεται στο δελτίο αποστολής. Η σουίτα προτείνει τιμή από την κατεύθυνση της διακίνησης και προσφέρει μόνο τους σκοπούς που της ταιριάζουν: τους εσωτερικούς όταν τα είδη πηγαίνουν σε δική σας θέση, τους υπόλοιπους όταν πηγαίνουν σε πελάτη ή προμηθευτή ή επιστρέφουν από αυτόν. Η παραλαβή από προμηθευτή δεν έχει σκοπό διακίνησης. Όπου ο σκοπός ισχύει, χωρίς αυτόν το δελτίο δεν εκδίδεται. Το field είναι επεξεργάσιμο, οπότε ένα module συνεργάτη μπορεί να το ορίζει όταν δημιουργεί διακινήσεις (π.χ. από δική του ροή)· η τιμή που ορίζετε μένει, αρκεί να είναι ένας από τους προσφερόμενους σκοπούς. Μετά την έκδοση του δελτίου η σουίτα δεν το ξαναϋπολογίζει. Δεν αντιγράφεται, άρα μια επιστροφή δεν κληρονομεί τον σκοπό της αρχικής. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_stock. Ορίστε το πριν από την έκδοση του δελτίου.
Παράδειγμα
Όταν οι πωλητές στέλνουν δείγματα προϊόντων σε υποψήφιους πελάτες, ένα κουμπί ορίζει ως σκοπό διακίνησης τον δειγματισμό, ώστε το δελτίο αποστολής να μη δηλώνει πώληση.
# Customer need: sales reps send product samples to prospects; the delivery note must say sampling, not sale.
# depends: ['stock', 'l10n_gr_nt_stock']
from odoo import _, models
from odoo.exceptions import UserError
class StockPicking(models.Model):
_inherit = 'stock.picking'
def action_acme_mark_as_samples(self):
# AADE table 8.14: code 3 is sampling.
sampling = self.env['aade.move.purpose'].search([('code', '=', 3)], limit=1)
for picking in self:
if picking.state in ('done', 'cancel'):
raise UserError(_('%s is already done or cancelled: set the purpose before shipping.', picking.name))
if sampling not in picking.allowed_move_purpose_ids:
raise UserError(_('%s cannot carry the sampling purpose for this direction.', picking.name))
# Kept as set, since it is one of the purposes offered for this picking.
picking.move_purpose_id = sampling
return Truestock.picking.vehicle_number
fields.Char(string='Vehicle Number', copy=False)Αριθμός κυκλοφορίας οχήματος. Ο αριθμός κυκλοφορίας του οχήματος που μεταφέρει τα είδη. Τυπώνεται στο δελτίο αποστολής και περιλαμβάνεται στα στοιχεία του. Χρησιμοποιήστε το όταν το module σας γνωρίζει το όχημα (σύνδεση με στόλο, προγραμματισμός δρομολογίων) και πρέπει να το συμπληρώνει αντί για τον χρήστη. Δεν αντιγράφεται, άρα επιστροφές και αντίγραφα ξεκινούν κενά. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_stock. Συμπληρώστε το πριν από την έκδοση του δελτίου.
Παράδειγμα
Ο υπεύθυνος αποστολών καταχωρίζει μία φορά τον αριθμό κυκλοφορίας του φορτηγού σε μια ομάδα παραδόσεων, και τον αναγράφει κάθε δελτίο αποστολής της ομάδας.
# Customer need: the dispatcher sets the truck once on a picking batch, and every delivery in the batch carries its plate.
# depends: ['stock_picking_batch', 'l10n_gr_nt_stock']
from odoo import fields, models
class StockPickingBatch(models.Model):
_inherit = 'stock.picking.batch'
x_license_plate = fields.Char(string='Truck plate')
def write(self, vals):
res = super().write(vals)
if 'x_license_plate' in vals:
# Only deliveries still to ship; done pickings keep what they declared.
self.picking_ids.filtered(lambda p: p.state not in ('done', 'cancel')).write({
'vehicle_number': vals['x_license_plate'],
})
return resΙσοζύγιο αποθήκης
depends: ['l10n_gr_nt_stock_balance']
Απογραφή έναρξης + εισαγωγές − εξαγωγές = υπόλοιπο, ανά είδος και αποθήκη, σε ποσότητα και αξία που συμφωνεί με την αποτίμηση του Odoo· προσθέτει και τις θέσεις για καταστροφές και για κλοπές και απώλειες αποθεμάτων.
stock.location.l10n_gr_destruction
fields.Boolean(string='Stock destruction', copy=False, readonly=True)Σήμανση θέσης καταστροφής αποθεμάτων. Σημειώνει τη θέση «Καταστροφές αποθεμάτων» που δημιουργεί η σουίτα μία φορά ανά εταιρεία, με λογαριασμό αποτίμησης 67.02.00, και την κάνει προεπιλεγμένη θέση σκάρτων της εταιρείας. Ο χρήστης μπορεί αργότερα να αλλάξει το όνομα ή τον λογαριασμό της· η σήμανση μένει, οπότε αναζητήστε τη θέση με τη σήμανση και ποτέ με το όνομα. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_stock_balance και εταιρεία με ελληνικό λογιστικό σχέδιο (ΕΛΠ) που έχει τον λογαριασμό 67.02.00· αλλιώς η θέση δεν δημιουργείται και η αναζήτηση δεν επιστρέφει τίποτα. Μια αρχειοθετημένη θέση δεν βρίσκεται με την προεπιλεγμένη αναζήτηση. Το field είναι μόνο για ανάγνωση: μην το ορίζετε σε άλλες θέσεις.
Παράδειγμα
Ένας διανομέας τροφίμων καταστρέφει με μία ενέργεια όλες τις ληγμένες παρτίδες και η ζημιά καταχωρείται στον λογαριασμό καταστροφής αποθεμάτων αντί για το κόστος πωληθέντων.
# Need: a food distributor destroys expired lots every month; the loss must land on 67.02.00.
# depends: ['l10n_gr_nt_stock_balance', 'product_expiry']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.l10n_gr_*.
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class StockQuant(models.Model):
_inherit = 'stock.quant'
@api.model
def _acme_destroy_expired_lots(self):
destruction = self.env['stock.location'].search([
('company_id', '=', self.env.company.id),
('l10n_gr_destruction', '=', True), # search by the marker, never by name
], limit=1)
if not destruction:
raise UserError(_('No stock destruction location. Is the Greek chart of accounts installed?'))
quants = self.search([
('company_id', '=', self.env.company.id),
('location_id.usage', '=', 'internal'),
('lot_id.expiration_date', '<', fields.Datetime.now()),
('quantity', '>', 0),
])
scraps = self.env['stock.scrap'].create([{
'product_id': q.product_id.id, 'lot_id': q.lot_id.id, 'scrap_qty': q.quantity,
'location_id': q.location_id.id, 'scrap_location_id': destruction.id,
'origin': _('Expired lots'),
} for q in quants])
for scrap in scraps: # action_validate works on one scrap at a time
scrap.action_validate()stock.location.l10n_gr_theft
fields.Boolean(string='Stock theft and loss', copy=False, readonly=True)Σήμανση θέσης κλοπών και απωλειών. Σημειώνει τη θέση «Κλοπές και απώλειες αποθεμάτων» που δημιουργεί η σουίτα μία φορά ανά εταιρεία, με λογαριασμό αποτίμησης 67.03.00. Χρησιμοποιήστε τη ως προορισμό όταν το module σας καταγράφει είδη που κλάπηκαν ή χάθηκαν, ώστε η ζημία να πηγαίνει στον σωστό λογαριασμό και όχι στο κόστος πωληθέντων. Ίδιες προϋποθέσεις με τη θέση καταστροφής: εγκατεστημένο το l10n_gr_nt_stock_balance, ελληνικό λογιστικό σχέδιο (ΕΛΠ) με τον λογαριασμό 67.03.00. Αναζητήστε τη με τη σήμανση και όχι με το όνομα. Το field είναι μόνο για ανάγνωση.
Παράδειγμα
Μια αλυσίδα λιανικής καταχωρεί τα κλεμμένα είδη με τον αριθμό της αστυνομικής δήλωσης, ώστε η ζημιά να πηγαίνει στον λογαριασμό κλοπής και απώλειας και όχι στο κόστος πωληθέντων.
# Need: a retail chain records goods found stolen at a store count, with the police report number.
# depends: ['l10n_gr_nt_stock_balance']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.l10n_gr_*.
from odoo import fields, models, _
from odoo.exceptions import UserError
class AcmeTheftReport(models.TransientModel):
_name = 'acme.theft.report'
_description = 'Record stolen goods'
product_id = fields.Many2one('product.product', required=True)
location_id = fields.Many2one('stock.location', required=True, domain=[('usage', '=', 'internal')])
quantity = fields.Float(required=True)
police_report = fields.Char(required=True)
def action_confirm(self):
self.ensure_one()
theft = self.env['stock.location'].search([
('company_id', '=', self.location_id.company_id.id),
('l10n_gr_theft', '=', True), # search by the marker, never by name
], limit=1)
if not theft:
raise UserError(_('No stock theft and loss location for this company.'))
self.env['stock.scrap'].create({
'product_id': self.product_id.id, 'scrap_qty': self.quantity,
'location_id': self.location_id.id, 'scrap_location_id': theft.id,
'origin': _('Police report %s', self.police_report),
}).action_validate()Μεταφορά αποθέματος μεταξύ ειδών
depends: ['l10n_gr_nt_stock_transfer']
Αριθμημένο παραστατικό αποθήκης που βγάζει ποσότητα από ένα είδος και τη βάζει σε άλλο, ως μετατροπή (η αξία μεταφέρεται) ή ως διόρθωση λάθους αποστολής (κάθε είδος στο δικό του κόστος, η διαφορά στο κόστος πωληθέντων), με υποχρεωτική αιτιολογία και αντιλογισμό αντί για διαγραφή.
l10n_gr.stock.product.transfer
models.Model, _inherit=['mail.thread'], _check_company_auto=True. Fields: name (Char, number, readonly), mode (Selection: 'conversion' | 'wrong_shipment'), company_id, date (Date), location_id (internal stock.location), reason (Text), state (Selection: 'draft' | 'posted' | 'reversed', readonly), reversal_of_id, reversed_by_id, line_ids (One2many to l10n_gr.stock.product.transfer.line), move_ids (One2many stock.move via l10n_gr_transfer_id)Παραστατικό μεταφοράς μεταξύ ειδών. Η κεφαλίδα μιας μεταφοράς από είδος σε είδος. Δημιουργείται ως πρόχειρο, παίρνει αριθμό με την καταχώρηση και δημιουργεί, για κάθε γραμμή, μία κίνηση που βγάζει το είδος πηγής από την επιλεγμένη εσωτερική θέση και μία που βάζει σε αυτήν το είδος προορισμού, μέσω θέσης μεταφοράς που κρατά η σουίτα ανά εταιρεία και τρόπο. Χρησιμοποιήστε το όταν το module σας πρέπει να μεταφέρει απόθεμα από έναν κωδικό είδους σε άλλον με κανονικό παραστατικό: ανασυσκευασία, κιβώτιο σε τεμάχια, αλλαγή κωδικού (mode 'conversion'), ή διόρθωση αποστολής λάθος είδους (mode 'wrong_shipment'). Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_stock_transfer· ο χρήστης χρειάζεται δικαιώματα Αποθήκη / Διαχειριστής· τα παραστατικά φιλτράρονται στις εταιρείες του χρήστη. Ένα καταχωρημένο παραστατικό δεν διαγράφεται: αντιλογίζεται. Μπορείτε να προσθέσετε δικά σας fields με _inherit.
Παράδειγμα
Ένας χονδρέμπορος συνδέει κάθε διόρθωση λανθασμένης αποστολής με την παραγγελία αγοράς του προμηθευτή και παρακολουθεί αν έχει σταλεί η απαίτηση.
# Need: a wholesaler corrects wrong-product shipments and must link each correction
# to the supplier's purchase order, so the claim to the supplier can be followed up.
# depends: ['l10n_gr_nt_stock_transfer', 'purchase']
# The l10n.gr API is reached through the ORM (_inherit), never by importing from odoo.addons.l10n_gr_*.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class StockProductTransfer(models.Model):
_inherit = 'l10n_gr.stock.product.transfer'
acme_purchase_id = fields.Many2one('purchase.order', string='Supplier order', check_company=True)
acme_claim_sent = fields.Boolean('Claim sent to supplier', tracking=True)
@api.constrains('mode', 'acme_purchase_id')
def _check_acme_purchase(self):
for transfer in self:
if transfer.mode == 'wrong_shipment' and not transfer.acme_purchase_id:
raise ValidationError(_('A wrong-shipment correction needs the supplier order.'))l10n_gr.stock.product.transfer.action_post
action_post(self) -> TrueΚαταχώρηση μεταφοράς. Ελέγχει και καταχωρεί πρόχειρα παραστατικά: ελέγχει την κεφαλίδα (κατάσταση πρόχειρο, mode, αιτιολογία, τουλάχιστον μία γραμμή, ημερομηνία όχι μελλοντική και όχι σε κλειδωμένη περίοδο) και κάθε γραμμή (διαφορετικά είδη, θετικές ποσότητες, είδη με απόθεμα που δεν παρακολουθούνται με σειριακό αριθμό ούτε αποτιμώνται ανά παρτίδα, παρτίδες όπου απαιτούνται, επαρκές διαθέσιμο υπόλοιπο μετά τις δεσμεύσεις). Η μετατροπή θέλει επιπλέον είδος προορισμού που δεν αποτιμάται σε πρότυπο κόστος και συμβατούς λογαριασμούς αποτίμησης· η διόρθωση λάθους αποστολής θέλει λογαριασμό κόστους πωληθέντων όπου η αποτίμηση είναι αυτόματη. Μετά αριθμεί το παραστατικό, δημιουργεί και επικυρώνει τις κινήσεις αποθήκης στην ημερομηνία του παραστατικού και το περνά σε κατάσταση posted. Σε κάθε αποτυχημένο έλεγχο σηκώνει UserError. Καλέστε το από το module σας αφού δημιουργήσετε μια μεταφορά. Για επέκταση, κάντε override και καλέστε super(): τους δικούς σας ελέγχους πριν από το super(), την επεξεργασία των κινήσεων που δημιουργήθηκαν μετά.
Παράδειγμα
Ένας διανομέας δεν επιτρέπει την καταχώριση διόρθωσης λανθασμένης αποστολής χωρίς φωτογραφία των ειδών και, μετά την καταχώριση, δημιουργεί εργασία για το τμήμα αγορών.
# Need: the warehouse manager requires a photo of the goods before a wrong-shipment
# correction is posted, and the purchasing team must be told after posting.
# depends: ['l10n_gr_nt_stock_transfer']
# The l10n.gr API is reached through the ORM (_inherit), never by importing from odoo.addons.l10n_gr_*.
from odoo import models, _
from odoo.exceptions import UserError
class StockProductTransfer(models.Model):
# the transfer has mail.thread only; add activities to schedule follow-ups
_inherit = ['l10n_gr.stock.product.transfer', 'mail.activity.mixin']
def action_post(self):
Attachment = self.env['ir.attachment']
wrong = self.filtered(lambda t: t.mode == 'wrong_shipment')
for transfer in wrong:
if not Attachment.search_count([('res_model', '=', self._name), ('res_id', '=', transfer.id)]):
raise UserError(_('Attach a photo of the goods to %s before posting.', transfer.display_name))
res = super().action_post() # suite checks, numbering and stock moves happen here
for transfer in wrong:
transfer.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Claim the wrong shipment from the supplier'),
user_id=transfer.create_uid.id,
)
return resl10n_gr.stock.product.transfer.action_reverse
action_reverse(self) -> dict (ir.actions.act_window opening the reversal)Αντιλογισμός καταχωρημένης μεταφοράς. Καλείται σε ένα καταχωρημένο παραστατικό (ensure_one). Δημιουργεί και καταχωρεί νέο παραστατικό με σημερινή ημερομηνία, ίδιου mode και ίδιας θέσης, με αντίθετη κατεύθυνση (από το είδος προορισμού πίσω στο είδος πηγής, με αντεστραμμένες ποσότητες και παρτίδες), συνδεδεμένο με το αρχικό μέσω reversal_of_id / reversed_by_id, και περνά το αρχικό σε κατάσταση reversed. Η αξία εξαγωγής του αντιλογισμού είναι το τρέχον κόστος του είδους, άρα δεν επαναφέρει ακριβώς τις αρχικές αξίες. Σηκώνει UserError αν το παραστατικό δεν είναι καταχωρημένο ή αν ο αντιλογισμός δεν περνά κάποιον έλεγχο καταχώρησης. Επιστρέφει window action προς τον αντιλογισμό. Κάντε override με super() αν χρειάζεται να μεταφέρετε δικά σας fields στον αντιλογισμό.
Παράδειγμα
Όταν αντιλογίζεται μια καταχωρισμένη μεταφορά, ο αντιλογισμός διατηρεί τη σύνδεση με την παραγγελία του προμηθευτή και ο λογιστής λαμβάνει εργασία να ελέγξει την αξία του.
# Need: reversals must keep the supplier order link and alert the accountant,
# because the reversal is valued at today's cost and the figures will differ.
# depends: ['l10n_gr_nt_stock_transfer', 'purchase']
# The l10n.gr API is reached through the ORM (_inherit), never by importing from odoo.addons.l10n_gr_*.
from odoo import fields, models, _
class StockProductTransfer(models.Model):
# the transfer has mail.thread only; add activities to schedule follow-ups
_inherit = ['l10n_gr.stock.product.transfer', 'mail.activity.mixin']
acme_purchase_id = fields.Many2one('purchase.order', string='Supplier order')
def action_reverse(self):
res = super().action_reverse() # works on one transfer and posts the reversal
reversal = self.reversed_by_id
reversal.acme_purchase_id = self.acme_purchase_id
accountant = self.env.ref('account.group_account_manager').user_ids[:1]
reversal.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Check the value of the reversal of %s', self.name),
user_id=(accountant or self.env.user).id,
)
return resl10n_gr.stock.product.transfer.line
models.Model, _check_company_auto=True. Fields: transfer_id (required, ondelete='cascade'), product_id, lot_id, quantity (source), dest_product_id, dest_lot_id, dest_quantity (computed from quantity, stored, editable), out_move_id, in_move_id (readonly), out_value, in_value, cogs_difference (computed Monetary)Γραμμή μεταφοράς. Ένα είδος πηγής με ποσότητα και παρτίδα που βγαίνει, και ένα είδος προορισμού με ποσότητα και παρτίδα που μπαίνει. Το dest_quantity ακολουθεί την ποσότητα πηγής όταν τα δύο είδη έχουν την ίδια μονάδα· όταν δεν έχουν, το ορίζετε εσείς (1 κιβώτιο σε 12 τεμάχια). Πριν από την καταχώρηση τα fields αξίας είναι εκτιμήσεις· μετά, τα out_move_id και in_move_id δείχνουν τις κινήσεις αποθήκης που δημιουργήθηκαν, και τα fields αξίας δείχνουν την αξία εξαγωγής, εισαγωγής και, στη διόρθωση λάθους αποστολής, τη διαφορά που πήγε στο κόστος πωληθέντων. Οι γραμμές δημιουργούνται μέσω του line_ids του παραστατικού.
Παράδειγμα
Ένας χονδρέμπορος μετατρέπει το εβδομαδιαίο αρχείο ανοίγματος κιβωτίων της αποθήκης σε ένα αριθμημένο παραστατικό μετατροπής, με έξοδο των κιβωτίων και είσοδο των τεμαχίων.
# Need: a cosmetics wholesaler splits boxes into pieces every week from a CSV the
# warehouse exports (box_sku;piece_sku;boxes;pieces_per_box). Run in odoo-bin shell (env given).
# depends (of the module the data belongs to): ['l10n_gr_nt_stock_transfer']
# The l10n.gr API is reached through the ORM (env[...]), never by importing from odoo.addons.l10n_gr_*.
import csv
from odoo import Command
Product = env['product.product']
stock = env['stock.warehouse'].search([('company_id', '=', env.company.id)], limit=1).lot_stock_id
with open('/tmp/box_split.csv', newline='', encoding='utf-8') as f:
rows = list(csv.DictReader(f, delimiter=';'))
lines = []
for row in rows:
box = Product.search([('default_code', '=', row['box_sku'])], limit=1)
piece = Product.search([('default_code', '=', row['piece_sku'])], limit=1)
if not (box and piece):
print('skipped, unknown item:', row['box_sku'], row['piece_sku'])
continue
boxes = float(row['boxes'])
lines.append(Command.create({
'product_id': box.id, 'quantity': boxes,
# different units (box -> pieces): set dest_quantity yourself
'dest_product_id': piece.id, 'dest_quantity': boxes * float(row['pieces_per_box']),
}))
transfer = env['l10n_gr.stock.product.transfer'].create({
'mode': 'conversion', 'location_id': stock.id,
'reason': 'Weekly box split', 'line_ids': lines,
})
transfer.action_post()
env.cr.commit()stock.move.l10n_gr_transfer_id
fields.Many2one('l10n_gr.stock.product.transfer', string='Product transfer', copy=False, index='btree_not_null', ondelete='restrict')Μεταφορά από την οποία προήλθε η κίνηση. Συμπληρώνεται στις κινήσεις αποθήκης που δημιουργεί μια μεταφορά μεταξύ ειδών· σε όλες τις άλλες κινήσεις είναι κενό. Χρησιμοποιήστε το για να ξεχωρίζετε αυτές τις κινήσεις στις αναφορές ή στη λογική σας (π.χ. για να εξαιρείτε τις μετατροπές από στατιστικά πωλήσεων). Όσο υπάρχουν κινήσεις που δείχνουν σε ένα παραστατικό, αυτό δεν διαγράφεται. Μην το ορίζετε σε δικές σας κινήσεις.
Παράδειγμα
Μια αναφορά απωλειών αποθέματος δείχνει τις διορθώσεις απογραφής και τις καταστροφές κάθε είδους για 90 ημέρες, χωρίς να μετρά τις εξαγωγές από μεταφορές είδους σε είδος.
# Need: a stock-loss report per item over 90 days (inventory adjustments and scrap).
# Product transfers also leave stock through an inventory-type location, but they are not losses.
# depends: ['l10n_gr_nt_stock_transfer']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.l10n_gr_*.
from datetime import timedelta
from odoo import fields, models
class ProductProduct(models.Model):
_inherit = 'product.product'
acme_loss_qty_90d = fields.Float('Lost in 90 days', compute='_compute_acme_loss_qty_90d')
def _compute_acme_loss_qty_90d(self):
since = fields.Datetime.now() - timedelta(days=90)
groups = self.env['stock.move']._read_group([
('product_id', 'in', self.ids),
('state', '=', 'done'),
('date', '>=', since),
('location_id.usage', '=', 'internal'),
('location_dest_id.usage', '=', 'inventory'),
('l10n_gr_transfer_id', '=', False), # leave product transfers out
], ['product_id'], ['product_qty:sum'])
qty = {product.id: total for product, total in groups}
for product in self:
product.acme_loss_qty_90d = qty.get(product.id, 0.0)Ενιαία Κωδικοποίηση Ειδών (TARIC)
depends: ['l10n_gr_nt_taric']
Εξασφαλίζει ότι κάθε είδος που διακινείται θα έχει έγκυρο δασμολογικό κωδικό TARIC πριν από την 1/1/2028, με τη βιβλιοθήκη της ΕΕ, προτάσεις από τον προμηθευτή και από AI, μαζική έγκριση και φραγμό στο δελτίο μετά την ημερομηνία υποχρέωσης.
l10n_gr.mydata.received.line.l10n_gr_taric_no
fields.Char(size=10), indexedΚωδικός TARIC προμηθευτή σε γραμμή ληφθέντος. Ο κωδικός TARIC που δήλωσε ο προμηθευτής σε γραμμή ληφθέντος παραστατικού. Τον συμπληρώνει η σουίτα όταν φέρνει το παραστατικό και ο προμηθευτής έχει στείλει κωδικό· αλλιώς μένει κενός. Είναι δήλωση του προμηθευτή, όχι ακόμη δικός σας κωδικός. Χρησιμοποιήστε τον για να προτείνετε ή να ελέγχετε κωδικούς στα είδη σας, για παράδειγμα αντιστοιχίζοντας γραμμές με είδη και επιβεβαιώνοντας τον κωδικό με το _l10n_gr_find στην ημερομηνία έκδοσης του παραστατικού. Τον διαβάζετε· μην αλλάζετε ό,τι έστειλε ο προμηθευτής. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_taric (φέρνει και το l10n_gr_nt_mydata_received).
Παράδειγμα
Μια προγραμματισμένη ενέργεια συμπληρώνει τους κωδικούς TARIC που λείπουν από τα είδη αγοράς με τους έγκυρους κωδικούς που αναγράφουν οι προμηθευτές στα ληφθέντα παραστατικά τους.
# Need: before 1/1/2028, fill the missing TARIC codes of purchased items from what the
# suppliers already send on their invoices, matched through the supplier's product code.
# depends: ['l10n_gr_nt_taric']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.l10n_gr_*.
from odoo import api, models, _
class ProductTemplate(models.Model):
_inherit = 'product.template'
@api.model
def _cron_acme_taric_from_suppliers(self):
Taric = self.env['l10n_gr.taric.code']
lines = self.env['l10n_gr.mydata.received.line'].search([
('l10n_gr_taric_no', '!=', False), ('item_code', '!=', False),
('doc_id.partner_id', '!=', False),
])
for line in lines:
info = self.env['product.supplierinfo'].search([
('partner_id', '=', line.doc_id.partner_id.id),
('product_code', '=', line.item_code),
], limit=1)
tmpl = info.product_tmpl_id
if tmpl and not tmpl.l10n_gr_taric_no and Taric._l10n_gr_find(line.l10n_gr_taric_no, on_date=line.doc_id.issue_date):
tmpl.l10n_gr_taric_no = line.l10n_gr_taric_no # we write our item, never the supplier's line
tmpl.message_post(body=_('TARIC code taken from %s.', line.doc_id.partner_id.display_name))l10n_gr.taric.code
models.Model, _order 'code, suffix'; fields: code (Char 10, required), suffix (Char 2, '80' = goods), name, name_en (Char), complete_name, complete_name_en (stored), level (Integer: 2 chapter ... 10 TARIC), parent_id / child_ids (hierarchy), chapter, heading (stored), declarable (Boolean), date_start, date_end (Date), active; unique (code, suffix)Βιβλιοθήκη TARIC. Η δασμολογική ονοματολογία της ΕΕ, που τη φορτώνει και την ανανεώνει η σουίτα. Κάθε εγγραφή είναι ένας κωδικός με την περιγραφή του στα ελληνικά και στα αγγλικά, τη θέση του στην ιεραρχία και τις ημερομηνίες ισχύος. Σε είδος μπαίνουν μόνο οι δηλώσιμοι κωδικοί (γραμμές αγαθών χωρίς υποδιαιρέσεις). Χρησιμοποιήστε το για αναζήτηση κωδικών, για έλεγχο κωδικών που λαμβάνει το module σας από αλλού, ή για εμφάνιση της πλήρους περιγραφής. Η αναζήτηση με όνομα ψάχνει στον κωδικό και στις δύο περιγραφές. Είναι δεδομένα αναφοράς: τα διαβάζετε, δεν τα αλλάζετε. Δικαιώματα: ανάγνωση για χρήστες Αποθήκης, Πωλήσεων και Τιμολόγησης· πλήρη δικαιώματα οι διαχειριστές Αποθήκης, για την ανανέωση της βιβλιοθήκης. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_taric.
Παράδειγμα
Ένας συνεργάτης που κάνει μετάπτωση από παλιό ERP μετατρέπει τους παλιούς οκταψήφιους δασμολογικούς κωδικούς σε κωδικούς TARIC και παίρνει λίστα με τα είδη που χρειάζονται απόφαση από άνθρωπο.
# Need: migration from a legacy ERP that kept 8-digit customs (CN) codes on items.
# Map each to the single declarable 10-digit TARIC code valid today under it; list the rest for review.
# Run in odoo-bin shell (env given). depends: ['l10n_gr_nt_taric']
# The l10n.gr API is reached through the ORM (env[...]), never by importing from odoo.addons.l10n_gr_*.
import csv
Taric = env['l10n_gr.taric.code']
review = []
with open('/tmp/legacy_items.csv', newline='', encoding='utf-8') as f:
for row in csv.DictReader(f, delimiter=';'): # columns: sku;cn8
tmpl = env['product.template'].search([('default_code', '=', row['sku'])], limit=1)
candidates = Taric.search([
('code', '=like', row['cn8'] + '%'),
('declarable', '=', True),
]).filtered(lambda c: Taric._l10n_gr_find(c.code)) # keep codes valid today
if tmpl and len(candidates) == 1:
tmpl.l10n_gr_taric_no = candidates.code
else:
review.append((row['sku'], row['cn8'], ', '.join(candidates.mapped('code'))))
env.cr.commit()
for sku, cn8, options in review:
print(sku, cn8, options or 'no declarable code')l10n_gr.taric.code._l10n_gr_find
@api.model _l10n_gr_find(self, code, on_date=None) -> l10n_gr.taric.code record or emptyΕύρεση έγκυρου κωδικού TARIC. Επιστρέφει την εγγραφή της βιβλιοθήκης για δηλώσιμο κωδικό που ισχύει την on_date (σήμερα αν δεν δοθεί), ή κενή εγγραφή όταν ο κωδικός είναι κενός, άγνωστος, μη δηλώσιμος ή δεν ισχύει εκείνη την ημερομηνία. Είναι η ίδια αναζήτηση με την οποία η σουίτα κρίνει τα είδη. Χρησιμοποιήστε τη όποτε το module σας χρειάζεται να ξέρει αν ένας κωδικός γίνεται δεκτός, για παράδειγμα σε εισαγωγή ειδών ή στον έλεγχο του κωδικού προμηθευτή σε γραμμή ληφθέντος. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_taric.
Παράδειγμα
Η εισαγωγή ειδών απορρίπτει κάθε κωδικό TARIC που δεν είναι δηλώσιμος και έγκυρος την ημερομηνία υποχρέωσης, ώστε λανθασμένοι κωδικοί να μη φτάνουν ποτέ στον κατάλογο ειδών.
# Need: items come in daily from a supplier price-list import; a code that is not valid
# on 1/1/2028 must be rejected at save time, not discovered when a delivery note is blocked.
# depends: ['l10n_gr_nt_taric']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.l10n_gr_*.
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
class ProductTemplate(models.Model):
_inherit = 'product.template'
@api.constrains('l10n_gr_taric_no')
def _check_acme_taric_valid(self):
Taric = self.env['l10n_gr.taric.code']
deadline = fields.Date.to_date('2028-01-01')
for tmpl in self.filtered('l10n_gr_taric_no'):
if not Taric._l10n_gr_find(tmpl.l10n_gr_taric_no, on_date=deadline):
raise ValidationError(_(
'%(item)s: TARIC code %(code)s is not a declarable code valid on 1/1/2028.',
item=tmpl.display_name, code=tmpl.l10n_gr_taric_no))product.template.l10n_gr_taric_code_id
fields.Many2one('l10n_gr.taric.code'), computed, not stored, depends on the company in contextΠεριγραφή TARIC του είδους. Η εγγραφή της βιβλιοθήκης που αντιστοιχεί σήμερα στον κωδικό TARIC του είδους, ή κενό όταν το είδος δεν έχει κωδικό ή ο κωδικός δεν είναι έγκυρος. Υπολογίζεται από τον κωδικό που είναι αποθηκευμένος στο είδος, οπότε το διαβάζετε για να δείξετε την επίσημη περιγραφή ή για να φτάσετε στην ιεραρχία· για να το αλλάξετε, αλλάζετε τον κωδικό του είδους. Δεν είναι αποθηκευμένο, άρα δεν μπαίνει σε domain αναζήτησης. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_taric.
Παράδειγμα
Ένας εξαγωγέας τυπώνει κάτω από κάθε είδος του δελτίου αποστολής τον κωδικό TARIC και την επίσημη περιγραφή του.
<!-- Need: an exporter's customers ask for the TARIC code and its official description
under each item on the validated delivery note. -->
<!-- depends: ['l10n_gr_nt_taric', 'stock'] -->
<odoo>
<!-- lines grouped by product (lots not printed) -->
<template id="acme_delivery_aggregated_taric" inherit_id="stock.stock_report_delivery_aggregated_move_lines">
<xpath expr="//tr/td[1]" position="inside">
<t t-set="taric" t-value="aggregated_lines[line]['product'].product_tmpl_id.with_company(o.company_id).l10n_gr_taric_code_id"/>
<div t-if="taric" class="text-muted small">
TARIC <span t-out="taric.code"/>: <span t-out="taric.complete_name"/>
</div>
</xpath>
</template>
<!-- one line per lot or serial number -->
<template id="acme_delivery_serial_taric" inherit_id="stock.stock_report_delivery_has_serial_move_line">
<xpath expr="//td[1]" position="inside">
<t t-set="taric" t-value="move_line.product_id.product_tmpl_id.with_company(o.company_id).l10n_gr_taric_code_id"/>
<div t-if="taric" class="text-muted small">
TARIC <span t-out="taric.code"/>: <span t-out="taric.complete_name"/>
</div>
</xpath>
</template>
</odoo>product.template.l10n_gr_taric_status
fields.Selection: missing, valid, ending, unknown; computed, not stored, depends on the company in contextΈλεγχος TARIC του είδους. Η κατάσταση του κωδικού του είδους: missing (χωρίς κωδικό), valid (έγκυρος), ending (έγκυρος σήμερα αλλά λήγει πριν την ημερομηνία «Υποχρέωση κωδικού TARIC από» της εταιρείας, από προεπιλογή 1/1/2028) ή unknown (δεν υπάρχει στη βιβλιοθήκη ή δεν ισχύει). Χρησιμοποιήστε το για να βρίσκετε είδη που θέλουν προσοχή πριν την ημερομηνία υποχρέωσης, σε δικές σας αναφορές, εισαγωγές ή ελέγχους. Δεν είναι αποθηκευμένο, οπότε φιλτράρετε τις εγγραφές σε Python και όχι σε domain. Προϋπόθεση: εγκατεστημένο το l10n_gr_nt_taric· η κατάσταση ending εξαρτάται από τη ρύθμιση εταιρείας «Υποχρέωση κωδικού TARIC από».
Παράδειγμα
Μια εβδομαδιαία προγραμματισμένη ενέργεια αναθέτει στον υπεύθυνο ειδών μία εργασία για κάθε είδος με κωδικό TARIC που λείπει, είναι άγνωστος ή λήγει πριν από την ημερομηνία υποχρέωσης.
# Need: every Monday until the obligation date, the product manager gets one task per
# item that still has a missing, unknown or ending TARIC code.
# depends: ['l10n_gr_nt_taric']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.l10n_gr_*.
from odoo import api, models, _
class ProductTemplate(models.Model):
_inherit = 'product.template'
@api.model
def _cron_acme_taric_todo(self):
manager = self.env.ref('stock.group_stock_manager').user_ids[:1] or self.env.user
summary = _('Fix the TARIC code')
items = self.search([('type', '=', 'consu'), ('sale_ok', '=', True)])
# the status is computed, not stored: filter in Python, not in the domain
todo = items.filtered(lambda t: t.l10n_gr_taric_status in ('missing', 'unknown', 'ending'))
open_ids = self.env['mail.activity'].search([
('res_model', '=', self._name), ('res_id', 'in', todo.ids), ('summary', '=', summary),
]).mapped('res_id')
for tmpl in todo - self.browse(open_ids):
tmpl.activity_schedule('mail.mail_activity_data_todo', summary=summary, user_id=manager.id)l10n.gr — Διαθεσιμότητα ανά Κατάστημα
depends: ['nt_multi_branch']
Κάνει είδη, τιμοκαταλόγους, συναλλασσόμενους και τρόπους πληρωμής ταμείου διαθέσιμους στα καταστήματα που επιλέγετε, με ρύθμιση εταιρείας για το τι βλέπουν τα υπόλοιπα. Δημιουργεί ταμείο για κάθε νέο κατάστημα και αυτόματη παραλαβή στις μεταφορές μεταξύ καταστημάτων.
nt.multi.branch.mixin
models.AbstractModel, _name='nt.multi.branch.mixin'. Adds nt_branch_ids and _nt_available_in(company)Mixin διαθεσιμότητας ανά κατάστημα. Το mixin που παρέχει το field «Διαθέσιμα καταστήματα». Το Odoo δίνει σε μια εγγραφή ένα company_id (κενό, μία εταιρεία, ή γονέας με τα branches του)· το mixin προσθέτει δίπλα του ένα ελεύθερο σύνολο καταστημάτων, χωρίς να αγγίζει το company_id ή τους ελέγχους εταιρείας του Odoo. Κάντε inherit το mixin στο δικό σας model για να πάρετε το ίδιο field και τη βοηθητική μέθοδο. Προϋπόθεση: εγκατεστημένο το nt_multi_branch (προσθέστε το στα depends). Δίνει μόνο το field και τη μέθοδο: οι κανόνες πρόσβασης που κρύβουν εγγραφές από άλλα καταστήματα υπάρχουν μόνο εκεί όπου το χρησιμοποιεί η σουίτα (είδη, παραλλαγές, τιμοκατάλογοι, συναλλασσόμενοι, τρόποι πληρωμής ταμείου), οπότε στο δικό σας model προσθέστε δικό σας ir.rule ή φίλτρο όπου χρειάζεται. Δώστε στο Many2many δικό του όνομα relation, όπως κάνει η σουίτα σε κάθε model.
Παράδειγμα
Μια αλυσίδα γυμναστηρίων ορίζει προγράμματα συνδρομής που διατίθενται μόνο στα καταστήματα που επιλέγονται σε κάθε πρόγραμμα.
# Need: a gym chain sells membership plans that exist only in some of its branches.
# depends: ['nt_multi_branch']
# The l10n.gr API is reached through the ORM (_inherit), never by importing from odoo.addons.*.
from odoo import fields, models
class AcmeMembershipPlan(models.Model):
_name = 'acme.membership.plan'
_description = 'Membership plan'
_inherit = ['nt.multi.branch.mixin']
name = fields.Char(required=True, translate=True)
monthly_fee = fields.Monetary(currency_field='currency_id')
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.company.currency_id)
# own relation name, as the suite does for each model
nt_branch_ids = fields.Many2many(relation='acme_membership_plan_branch_rel')
# The mixin adds no record rule on your model. To hide plans from other branches, add one:
# domain_force: ['|', ('nt_branch_ids', '=', False), ('nt_branch_ids', 'in', company_ids)]nt.multi.branch.mixin._nt_available_in
_nt_available_in(self, company) -> boolΔιαθεσιμότητα εγγραφής σε κατάστημα. Για μία εγγραφή (ensure_one): True όταν το nt_branch_ids είναι κενό ή περιέχει τη δοσμένη εταιρεία, αλλιώς False. Χρησιμοποιήστε το σε ελέγχους Python, π.χ. για να σταματήσετε πώληση, εισαγωγή ή κλήση API για είδος που το κατάστημα δεν πωλεί· το ίδιο κάνει η σουίτα στις γραμμές παραγγελίας πώλησης.
Παράδειγμα
Η εισαγωγή παραγγελίας EDI για ένα κατάστημα κρατά μόνο τις γραμμές με είδη που πουλά το κατάστημα και επιστρέφει στον πελάτη τους υπόλοιπους κωδικούς ως απορριφθέντες.
# Need: B2B orders arrive by EDI for a given branch; lines for products that branch does
# not sell are returned to the customer as rejected instead of failing the whole order.
# depends: ['nt_multi_branch', 'sale']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.*.
from odoo import Command, api, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.model
def _acme_create_from_edi(self, branch, partner, edi_lines):
"""edi_lines: [{'sku': ..., 'qty': ...}]. Returns (order, rejected skus)."""
accepted, rejected = [], []
for row in edi_lines:
product = self.env['product.product'].search([('default_code', '=', row['sku'])], limit=1)
if product and product.product_tmpl_id._nt_available_in(branch):
accepted.append(Command.create({'product_id': product.id, 'product_uom_qty': row['qty']}))
else:
rejected.append(row['sku'])
order = self.with_company(branch).create({
'company_id': branch.id, 'partner_id': partner.id, 'order_line': accepted,
}) if accepted else self.browse()
return order, rejectednt.multi.branch.mixin.nt_branch_ids
fields.Many2many('res.company', string='Available branches')Διαθέσιμα καταστήματα. Τα καταστήματα όπου η εγγραφή είναι διαθέσιμη. Κενό σημαίνει όλα, όπως το κενό company_id στο Odoo σημαίνει όλες τις εταιρείες, άρα ένα κατάστημα που θα ανοίξει αργότερα βλέπει την εγγραφή χωρίς να την προσθέσει κανείς. Για να βρείτε τι μπορεί να χρησιμοποιήσει ένα κατάστημα, χρησιμοποιήστε domain της μορφής «κενό ή περιέχει το κατάστημα».
Παράδειγμα
Στην προσφορά, το πεδίο τιμοκαταλόγου προτείνει μόνο τους τιμοκαταλόγους που διατίθενται στο κατάστημα της συγκεκριμένης παραγγελίας.
# Need: a salesperson who works for several branches must see on a quotation only the
# pricelists available in the branch of that order, not in the branches selected in the switcher.
# depends: ['nt_multi_branch', 'sale']
# The l10n.gr API is reached through the ORM (_inherit), never by importing from odoo.addons.*.
from odoo import fields, models
class SaleOrder(models.Model):
_inherit = 'sale.order'
# «empty or contains the branch»: empty means available in every branch
pricelist_id = fields.Many2one(
domain="['|', ('company_id', '=', False), ('company_id', 'parent_of', company_id),"
" '|', ('nt_branch_ids', '=', False), ('nt_branch_ids', 'in', [company_id])]",
)product.template.nt_branch_ids
fields.Many2many('res.company', relation='nt_branch_product_template_rel'), default from the companies selected in the company switcher (empty when all the user's companies are selected); product.product exposes it as a related fieldΚαταστήματα όπου είναι διαθέσιμο ένα είδος. Το field του mixin, όπως εφαρμόζεται στα είδη. Ένα νέο είδος παίρνει ως προεπιλογή τα καταστήματα που είναι επιλεγμένα στην εναλλαγή εταιρείας, ή μένει κενό (όλα τα καταστήματα) όταν είναι επιλεγμένες όλες οι εταιρείες του χρήστη. Το ταμείο κάθε καταστήματος φορτώνει μόνο τα είδη που είναι διαθέσιμα σε αυτό, και η γραμμή παραγγελίας πώλησης ενός καταστήματος δεν δέχεται είδος που δεν είναι διαθέσιμο εκεί. Τι βλέπουν τα υπόλοιπα καταστήματα εξαρτάται από τη ρύθμιση «Είδη εκτός καταστήματος» της κεντρικής (ριζικής) εταιρείας: είτε το είδος δεν πωλείται αλλά το απόθεμα φαίνεται, είτε δεν φαίνεται καθόλου. Γράψτε το από module εισαγωγής ή συγχρονισμού για να ορίσετε τη διαθεσιμότητα· αφήστε το κενό για είδη που πωλούν όλα τα καταστήματα.
Παράδειγμα
Ένας συνεργάτης που κάνει μετάπτωση από παλιό ERP ορίζει ανά είδος τα καταστήματα που το πουλούν και το αφήνει διαθέσιμο σε όλα όταν το παλιό σύστημα δεν είχε περιορισμό.
# Need: migration from a legacy ERP that kept, per item, the stores that sell it
# (sku;stores, stores as 'ATH|PAT'). Empty in the file means every branch.
# Run in odoo-bin shell (env given). depends: ['nt_multi_branch']
# The l10n.gr API is reached through the ORM (env[...]), never by importing from odoo.addons.*.
import csv
from odoo import Command
# legacy store code -> branch company; adjust the names to the customer's branches
branches = {
'ATH': env['res.company'].search([('name', '=', 'Acme Αθήνα')], limit=1),
'PAT': env['res.company'].search([('name', '=', 'Acme Πάτρα')], limit=1),
}
with open('/tmp/item_stores.csv', newline='', encoding='utf-8') as f:
for row in csv.DictReader(f, delimiter=';'):
tmpl = env['product.template'].search([('default_code', '=', row['sku'])], limit=1)
if not tmpl:
continue
codes = [c for c in row['stores'].split('|') if c]
ids = [branches[c].id for c in codes if branches.get(c)]
tmpl.nt_branch_ids = [Command.set(ids)] if ids else [Command.clear()]
env.cr.commit()res.company._nt_branch_pos_vals
_nt_branch_pos_vals(self, source, vals) -> dictΤιμές του ταμείου νέου καταστήματος. Όταν δημιουργείται νέο κατάστημα που δεν έχει ακόμη ταμείο, και η κεντρική (ριζική) εταιρεία έχει, η σουίτα αντιγράφει κατά το commit το πρώτο pos.config της κεντρικής στο κατάστημα, με τη δική του αποθήκη και αντίγραφα των τρόπων πληρωμής. Το hook δέχεται το pos.config πηγής και τις τιμές που ετοιμάστηκαν για το αντίγραφο, και επιστρέφει τις τιμές που θα χρησιμοποιηθούν· το self είναι το νέο κατάστημα. Κάντε override για να προσαρμόσετε ό,τι πρέπει να ανήκει στο κατάστημα και δεν αντιγράφεται αυτούσιο (π.χ. δικά σας Many2one fields στο pos.config προς εγγραφές εταιρείας). Καλείτε πάντα πρώτα το super() και επιστρέφετε το αποτέλεσμά του με τις αλλαγές σας: το ίδιο hook το χρησιμοποιούν και άλλα modules της σουίτας για τα δικά τους fields.
Παράδειγμα
Το νέο κατάστημα μιας αλυσίδας αρτοποιείων αποκτά POS που τυπώνει στις αποδείξεις την επωνυμία και τη διεύθυνση του καταστήματος και όχι του κεντρικού.
# Need: a bakery chain opens new branches often; each new branch's POS must print its
# own name and address on the receipt, not those of the head store it was copied from.
# depends: ['nt_multi_branch', 'point_of_sale']
# The l10n.gr API is reached through the ORM (_inherit), never by importing from odoo.addons.*.
from odoo import models, _
class ResCompany(models.Model):
_inherit = 'res.company'
def _nt_branch_pos_vals(self, source, vals):
vals = super()._nt_branch_pos_vals(source, vals) # always first: other modules use this hook
address = ', '.join(filter(None, [self.street, self.zip, self.city]))
vals.update({
'name': _('%s POS', self.name),
'is_header_or_footer': True,
'receipt_header': '%s\n%s' % (self.name, address),
})
return valsstock.picking.nt_transit_source_id
fields.Many2one('stock.picking', string='Sent from', readonly=True, copy=False, index='btree_not_null')Παράδοση αποστολής της παραλαβής καταστήματος. Το Odoo στέλνει μια παράδοση προς άλλη εταιρεία της ίδιας οντότητας στη θέση διαμετακόμισης, αλλά δεν δημιουργεί την παραλαβή στην άλλη πλευρά. Με το nt_multi_branch, η επικύρωση μιας παράδοσης προς αυτή τη θέση, με συναλλασσόμενο άλλη εταιρεία κάτω από την ίδια κεντρική (ριζική) εταιρεία, δημιουργεί την παραλαβή στην αποθήκη της εταιρείας προορισμού, έτοιμη να επικυρωθεί εκεί· αυτό το field στην παραλαβή δείχνει πίσω στην παράδοση αποστολής. Μεταφέρονται μόνο ποσότητες, όχι παρτίδες ή σειριακοί αριθμοί. Χρησιμοποιήστε το για να μεταβαίνετε από μια παράδοση στην παραλαβή της (αναζήτηση στο field) ή από μια παραλαβή στην προέλευσή της, π.χ. για να παρακολουθείτε είδη που βρίσκονται ακόμη σε διαμετακόμιση. Προϋπόθεση: η εταιρεία προορισμού έχει αποθήκη, αλλιώς η επικύρωση της παράδοσης σταματά με σφάλμα. Το ορίζει μόνο η σουίτα.
Παράδειγμα
Μια προγραμματισμένη ενέργεια επισημαίνει κάθε παραλαβή μεταξύ καταστημάτων που παραμένει ανοιχτή τρεις ημέρες μετά την επικύρωση της αποστολής.
# Need: goods sent between branches must be received within 3 days; the receiving
# branch's warehouse manager gets a task for every receipt still open after that.
# depends: ['nt_multi_branch', 'stock']
# The l10n.gr API is reached through the ORM, never by importing from odoo.addons.*.
from datetime import timedelta
from odoo import api, fields, models, _
class StockPicking(models.Model):
_inherit = 'stock.picking'
@api.model
def _cron_acme_late_branch_receipts(self):
late = self.search([
('nt_transit_source_id', '!=', False),
('nt_transit_source_id.date_done', '<', fields.Datetime.now() - timedelta(days=3)),
('state', 'not in', ('done', 'cancel')),
('activity_ids', '=', False),
])
managers = self.env.ref('stock.group_stock_manager').user_ids
for receipt in late:
manager = receipt.user_id or managers.filtered(lambda u: receipt.company_id in u.company_ids)[:1]
receipt.activity_schedule(
'mail.mail_activity_data_todo',
summary=_('Goods sent with %s not received yet', receipt.nt_transit_source_id.name),
user_id=(manager or self.env.user).id,
)