Όποιος τιμολογεί στο Odoo από παραγγελίες, tickets helpdesk και καταγραφή χρόνου, γνωρίζει το αποτέλεσμα: ένας πελάτης δεν λαμβάνει στο τέλος του μήνα ένα τιμολόγιο, αλλά πέντε. Κάθε ένα προέρχεται από διαφορετική παραγγελία ή ticket, κάθε ένα είναι επαγγελματικά σωστό – και παρ’ όλα αυτά ο πελάτης ρωτά αν δεν θα χωρούσαν όλα σε ένα έγγραφο.
Ακριβώς γι’ αυτό δεν υπάρχει στο Odoo 19 Community καθαρή λύση. Η γνωστή ενότητα OCA account_invoice_merge δεν έχει ακόμη κυκλοφορήσει για το Odoo 19 και επιλύει το πρόβλημα με τρόπο που αποτελεί σοβαρό κίνδυνο για τους παρόχους υπηρεσιών: οι ίδιες γραμμές συγχωνεύονται και έτσι χάνεται η σύνδεση προς την παραγγελία, την εργασία και τον καταγεγραμμένο χρόνο.
Για τη δική μας εγκατάσταση Odoo 19 Community αναπτύξαμε γι’ αυτό την ενότητα Merge Draft Invoices. Επειδή αυτό το πρόβλημα δεν αφορά μόνο εμάς, τη διαθέτουμε δωρεάν στο GitHub με την ελεύθερη άδεια AGPL-3.
Στο έργο στο GitHub:
Γιατί δική μας ενότητα αντί για τη λύση OCA;
Το OCA συντηρεί εδώ και πολλά χρόνια το account_invoice_merge στο αποθετήριο OCA/account-invoicing. Κατά τη στιγμή αυτού του άρθρου η μετάβαση στο 19.0 είναι εκεί ακόμη ανοιχτό Pull Request, όχι δημοσιευμένη ενότητα.
Σημαντικότερο όμως είναι ότι οι δύο ενότητες κάνουν διαφορετικά πράγματα:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| Αποτέλεσμα | συγχωνεύεται σε ένα από τα επιλεγμένα τιμολόγια | πάντα ένα νέο τιμολόγιο |
| Γραμμές | οι ίδιες γραμμές συγχωνεύονται, οι ποσότητες προστίθενται | κάθε γραμμή μεταφέρεται ένα προς ένα |
| Προέλευση | invoice_origin / payment_reference συνενώνονται στην κεφαλίδα | πεδία προέλευσης ανά γραμμή αντιγράφονται μέσω του δικού του hook _copy_data_extend_business_fields του Odoo |
| Καταγραφή χρόνου | δεν λαμβάνεται υπόψη | το timesheet_invoice_id μεταφέρεται στο νέο τιμολόγιο |
| Αναγνώσιμο για τον πελάτη | τίποτα | μια επικεφαλίδα πάνω από τις γραμμές κάθε πηγής με παραγγελία και ticket |
| Τιμολόγια πηγής | ακυρώνονται | ακύρωση (προεπιλογή), διαγραφή ή διατήρηση – με σαφώς ονομασμένες συνέπειες |
Η συγχώνευση ίδιων γραμμών είναι η καθοριστική διαφορά. Μια συγχωνευμένη γραμμή μπορεί να φέρει μόνο μία προέλευση. Μόλις δύο γραμμές από δύο διαφορετικές παραγγελίες φαίνονται ίδιες, η σύνδεση από το τιμολόγιο προς την παραγγελία, την εργασία και την καταγραφή χρόνου χάνεται. Σε μια επιχείρηση παροχής υπηρεσιών, όπου η τιμολόγηση γίνεται μέσω Ticket → Καταγραφή χρόνου → Τιμολόγιο, αυτό σημαίνει: οι ίδιες ώρες χρεώνονται δύο φορές ή εξαφανίζονται.
Η ενότητά μας δεν συγχωνεύει γι’ αυτό καταρχήν τίποτα. Το συγχωνευμένο τιμολόγιο έχει ακριβώς τόσες γραμμές όσες έχουν συνολικά οι πηγές του.
Δεν υιοθετήθηκε κώδικας από την ενότητα OCA. Και οι δύο ενότητες διατίθενται με AGPL-3.
Τι κάνει η ενότητα
Στην προβολή λίστας τιμολογίων επισημάνετε δύο ή περισσότερα προσχέδια, επιλέξτε στο μενού ενεργειών Συγχώνευση προσχεδίων τιμολογίων – ένας οδηγός δείχνει την επιλογή, τους ελέγχους και τις επιλογές για τα τιμολόγια πηγής. Μετά την επιβεβαίωση μεταβαίνετε απευθείας στο νέο τιμολόγιο.
Πριν από τη συγχώνευση η ενότητα ελέγχει, και μάλιστα όλα τα σημεία ταυτόχρονα, ώστε να μην βλέπει κανείς το επόμενο σφάλμα μετά από κάθε διόρθωση:
- επιλέχθηκαν τουλάχιστον δύο τιμολόγια
- όλα σε κατάσταση Προσχέδιο
- κανένα δεν έχει καταχωριστεί ποτέ (ένα παραστατικό που επαναφέρθηκε σε προσχέδιο διατηρεί το ιστορικό καταχωρίσεών του και απορρίπτεται)
- κανένα δεν έχει ήδη συγχωνευτεί
- όλα είναι τιμολόγιο, τιμολόγιο προμηθευτή, πιστωτικό σημείωμα ή απόδειξη – όχι απλά λογιστικά άρθρα
- ίδιος συνεργάτης, τύπος παραστατικού, εταιρεία, νόμισμα και ημερολόγιο
Διαφορές στους όρους πληρωμής, στη φορολογική αντιστοίχιση, στη διεύθυνση παράδοσης, στον τραπεζικό λογαριασμό ή στον πωλητή δεν εμποδίζουν. Ο οδηγός προειδοποιεί και υιοθετεί την τιμή του παλαιότερου επιλεγμένου τιμολογίου.
Τι διατηρείται
Ο πυρήνας της ενότητας είναι ότι κάθε σύνδεση που έχει μια γραμμή τιμολογίου με την προέλευσή της υπάρχει και στο νέο τιμολόγιο:
| Σύνδεση | Χειρισμός |
|---|---|
| Γραμμές παραγγελιών πώλησης (sale_line_ids) | αντιγράφονται ανά γραμμή μέσω του _copy_data_extend_business_fields του Odoo |
| Γραμμές παραγγελιών αγοράς (purchase_line_id) | ομοίως |
| Κατανομή κέντρων κόστους (analytic_distribution) | αντιγράφεται ανά γραμμή |
| άλλα πεδία προέλευσης ανά γραμμή | αυτόματα, εφόσον η ενότητα που τα παρέχει υλοποιεί το τεκμηριωμένο hook του Odoo |
| Προέλευση και αναφορά στην κεφαλίδα του τιμολογίου | συγχωνεύονται, αφαιρούνται οι διπλοεγγραφές, επεξεργάσιμα στον οδηγό |
| Παραγγελία πώλησης: invoice_ids, invoice_status, qty_invoiced | υπολογίζονται από τις γραμμές – η παραγγελία αναγνωρίζει από μόνη της το νέο τιμολόγιο |
| Καταγραφή χρόνου (timesheet_invoice_id) | μεταφέρεται στο νέο τιμολόγιο προτού καταστούν άκυρες οι πηγές |
| Ticket helpdesk | συνδέεται με την καταγραφή χρόνου και δείχνει έτσι αυτόματα στο νέο τιμολόγιο |
| Chatter | μήνυμα στο νέο τιμολόγιο με όλες τις πηγές, μήνυμα σε κάθε πηγή με σύνδεσμο προς το νέο |
| merged_into_move_id / merged_from_move_ids | αποθηκευμένη σύνδεση και προς τις δύο κατευθύνσεις |
Οι γραμμές φόρου, οι γραμμές όρων πληρωμής, οι γραμμές στρογγυλοποίησης και έκπτωσης για πρόωρη πληρωμή δεν αντιγράφονται, αλλά υπολογίζονται εκ νέου από το Odoo με βάση το περιεχόμενο. Το συγχωνευμένο τιμολόγιο έχει επομένως μία γραμμή φόρου ανά φορολογικό συντελεστή, όχι μία ανά πηγή. Δημιουργείται χωρίς αρίθμηση ως προσχέδιο· η ίδια η συγχώνευση δεν καταναλώνει αριθμό παραστατικού.
Επικεφαλίδες: ο πελάτης βλέπει από πού προέρχεται κάθε γραμμή
Αυτή είναι η λειτουργία που κάνει τη διαφορά στην καθημερινότητα. Πάνω από τις γραμμές κάθε τιμολογίου πηγής η ενότητα τοποθετεί μια γραμμή ενότητας – το ίδιο στοιχείο που χρησιμοποιεί το ίδιο το Odoo για την ενότητα «Προκαταβολές»:
BE10181 <- Επικεφαλίδα
22,500 x Ώρα τεχνικού 3.577,50
Μερικό σύνολο 3.577,50
BE10182 · Ticket: HT00219 <- Επικεφαλίδα
3,000 x Ώρα τεχνικού 477,00
Μερικό σύνολο 477,00
ΚΑΘΑΡΟ ΠΟΣΟ 4.054,50
Η επικεφαλίδα εκτυπώνεται στο έγγραφο που λαμβάνει ο πελάτης και η τυπική αναφορά τιμολογίου του Odoo προσθέτει από μόνη της ένα μερικό σύνολο ανά ενότητα. Έτσι ο πελάτης βλέπει και πόσο κόστισε κάθε μέρος.
Η περιγραφή συντίθεται από όσα έχει πράγματι η πηγή:
- οι παραγγελίες, απευθείας από τις συνδέσεις γραμμών – τα ονόματα είναι έτσι ακριβή· αν λείπουν συνδέσεις παραγγελιών, χρησιμοποιείται το ελεύθερο κείμενο από το invoice_origin
- τα tickets, που προσδιορίζονται μέσω της καταγραφής χρόνου – εμφανίζονται ως Ticket: HT00219 ή Tickets: …, με όριο πέντε συν «και N ακόμη»
- αν δεν υπάρχει τίποτα από τα δύο: ο αριθμός του τιμολογίου πηγής, έπειτα η αναφορά του
- αν δεν υπάρχει τίποτα απολύτως: ένα ουδέτερο «Λοιπές γραμμές», ώστε οι γραμμές μιας πηγής να μην εξαφανίζονται ποτέ σιωπηρά στο πάνω μπλοκ
Ό,τι λείπει παραλείπεται, δεν επινοείται. Το ticket εκτυπώνεται με τον δικό του αριθμό – αριθμός helpdesk_mgmt, ticket_ref του Enterprise ή το εμφανιζόμενο όνομα του μοντέλου – ποτέ με το ID της βάσης δεδομένων.
Οι επικεφαλίδες γράφονται στη γλώσσα του πελάτη, όπως το κάνει το Odoo κατά τη δημιουργία τιμολογίων από παραγγελίες. Το Chatter παραμένει στη γλώσσα του χρήστη.
Μια επικεφαλίδα δεν φέρει προϊόν, ποσότητα, τιμή, φόρο, λογαριασμό ούτε κατανομή κέντρων κόστους. Δεν μπορεί να επηρεάσει τα σύνολα, τη φορολογική βάση και τη λογιστική κόστους. Όποιος απενεργοποιήσει τις επικεφαλίδες στον οδηγό, λαμβάνει ακριβώς το τιμολόγιο που θα προέκυπτε χωρίς αυτή τη λειτουργία – ένα τεστ συγκρίνει και τα δύο αποτελέσματα μέχρι το τελευταίο λεπτό.
Τι συμβαίνει με τα τιμολόγια πηγής
Ο οδηγός προσφέρει τρεις επιλογές:
- Ακύρωση (προεπιλογή, συνιστάται). Η ακύρωση ενός προσχεδίου δεν έχει συνέπειες: ούτε αριθμός παραστατικού, ούτε καταχώριση. Τα τιμολόγια παραμένουν στη βάση δεδομένων ως ακυρωμένα παραστατικά με Chatter και σύνδεσμο προς το νέο τιμολόγιο. Το qty_invoiced στην παραγγελία πώλησης προσπερνά τα ακυρωμένα παραστατικά, τίποτα δεν μετράται διπλά. Αυτή είναι η επιλογή που παραμένει κατανοητή και μετά από έξι μήνες.
- Διαγραφή. Αμετάκλητη. Το Chatter και η αναφορά προς τα πίσω χάνονται μαζί με την εγγραφή· παραμένει μόνο το μήνυμα στο νέο τιμολόγιο. Χρήσιμη για προσχέδια μίας χρήσης που δεν πρέπει να τα βλέπει πια κανείς.
- Διατήρηση ως προσχέδιο. Οι πηγές παραμένουν ανοιχτές. Οι παραγγελίες πώλησής τους μετρούν τότε την πηγή και το νέο τιμολόγιο ως τιμολογημένα – η εργασία φαίνεται να έχει χρεωθεί διπλά, μέχρι κάποιος να καθαρίσει τα προσχέδια με το χέρι. Προορίζεται για τη σπάνια περίπτωση που θέλει κανείς να συγκρίνει πριν αποφασίσει. Ένα τεστ καταγράφει ακριβώς αυτή τη διπλή καταμέτρηση, ώστε να μην εκπλήξει κανέναν.
Όρια – τι δεν κάνει σκόπιμα η ενότητα
Παρακαλούμε διαβάστε πριν από την παραγωγική χρήση:
- Μόνο προσχέδια. Τα καταχωρισμένα τιμολόγια δεν συγχωνεύονται. Αυτό είναι θέμα πιστωτικών σημειωμάτων, όχι διαδικασία αντιγραφής.
- Προσχέδια που είχαν καταχωριστεί μία φορά και επαναφέρθηκαν απορρίπτονται. Έχουν αριθμό και ιστορικό καταχωρίσεων· το να κρυφτεί αυτό σε άλλο έγγραφο δεν είναι κάτι που πρέπει να κάνει σιωπηρά μια μαζική ενέργεια.
- Καμία συγχώνευση μεταξύ διαφορετικών συνεργατών, νομισμάτων, εταιρειών ή ημερολογίων. Δεν υπάρχει γι’ αυτό λογική μοναδική τιμή, γι’ αυτό η ενότητα απορρίπτει αντί να μαντεύει.
- Το payment_reference δεν υιοθετείται. Οι δομημένες αναφορές πληρωμής προσδιορίζουν ένα παραστατικό· δύο συνενωμένες δεν ταιριάζουν πουθενά στην τράπεζα. Το νέο τιμολόγιο λαμβάνει τη δική του κατά την καταχώριση.
- Οι γραμμές προκαταβολών αντιγράφονται όπως κάθε άλλη γραμμή. Εάν οι πηγές περιέχουν γραμμές προκαταβολών από το sale, το αποτέλεσμα πρέπει να ελεγχθεί πριν από την καταχώριση.
- Μόνο το timesheet_invoice_id μεταφέρεται αυτόματα. Κάθε άλλο μοντέλο με Many2one προς account.move (δικές του τρεξίματα τιμολόγησης, συνδέσεις συνδρομών, διαβιβάσεις EDI) εξακολουθεί να δείχνει στην πηγή. Γι’ αυτό υπάρχει το hook _merge_draft_relink.
- Καμία συγχώνευση ίδιων γραμμών – σκόπιμα. Όποιος θέλει ένα σύντομο, τακτοποιημένο τιμολόγιο, βρίσκεται εδώ σε λάθος μέρος και θα το πλήρωνε με τις συνδέσεις προέλευσης.
- Οι επικεφαλίδες δημιουργούνται μία φορά κατά τη συγχώνευση. Στη συνέχεια είναι κανονικές γραμμές ενότητας: επεξεργάσιμες, διαγράψιμες και δεν ενημερώνονται όταν η παραγγελία ή το ticket μετονομαστεί αργότερα.
- Μια επικεφαλίδα αναφέρει τα tickets ολόκληρου του τιμολογίου πηγής, όχι της μεμονωμένης γραμμής.
Εγκατάσταση στο Odoo 19 Community
Η εγκατάσταση γίνεται όπως σε μια συνηθισμένη πρόσθετη ενότητα του Odoo:
- Αντιγράψτε τον κατάλογο της ενότητας σε μια ρυθμισμένη διαδρομή addons.
- Ενημερώστε τη λίστα εφαρμογών στο Odoo.
- Εγκαταστήστε την ενότητα account_invoice_merge_draft.
Η ενότητα εξαρτάται μόνο από το account. Ό,τι γνωρίζει για τα sale, purchase, project, hr_timesheet, sale_timesheet ή μια ενότητα helpdesk, εντοπίζεται κατά τον χρόνο εκτέλεσης – εγκαθίσταται και λειτουργεί ακόμη και σε μια καθαρή βάση δεδομένων τιμολόγησης.
Στη συνέχεια: Λογιστική → Πελάτες → Τιμολόγια (ή Προμηθευτές → Τιμολόγια), επισημάνετε δύο ή περισσότερα προσχέδια, μενού με το γρανάζι → Συγχώνευση προσχεδίων τιμολογίων. Η ενέργεια είναι διαθέσιμη σε όλους τους χρήστες με την ομάδα Τιμολόγηση / Χρέωση.
Όποιος ενημερώνει από προηγούμενη έκδοση της ενότητας, θα πρέπει μία φορά να κάνει ενημέρωση με -u account_invoice_merge_draft --i18n-overwrite, ώστε οι αλλαγμένες ονομασίες πεδίων να περάσουν στη γερμανική μετάφραση. Μια νέα εγκατάσταση δεν επηρεάζεται.
Για προγραμματιστές: API και hooks
Η συγχώνευση μπορεί να κληθεί και από δικό σας κώδικα:
python
merged = draft_moves.merge_draft_moves(
extra_vals={"invoice_date": date(2026, 3, 31), "ref": "Q1"},
source_handling="cancel", # "delete" ή "keep"
add_source_sections=True, # επικεφαλίδες· προεπιλογή True
)
Το merge_draft_moves εγείρει ένα UserError με όλους τους λόγους απόρριψης, όταν η επιλογή δεν μπορεί να συγχωνευθεί. Το _merge_draft_get_errors() επιστρέφει τους ίδιους λόγους ως λίστα χωρίς εξαίρεση.
Όποιος έχει δικές του αναφορές προς τα πίσω στο account.move, παρακάμπτει το _merge_draft_relink:
python
class AccountMove(models.Model):
_inherit = "account.move"
def _merge_draft_relink(self, new_move):
super()._merge_draft_relink(new_move)
if "my_move_id" not in self.env["my.model"]._fields:
return
self.env["my.model"].search([("my_move_id", "in", self.ids)]).write(
{"my_move_id": new_move.id}
)
Όποιος έχει αναφορά προς τα πίσω στη γραμμή τιμολογίου, δεν χρειάζεται καθόλου αυτή την ενότητα: αρκεί το ίδιο το hook του Odoo account.move.line._copy_data_extend_business_fields και το πεδίο υιοθετείται εξίσου κατά τη συγχώνευση, την ακύρωση και τον διπλασιασμό. Για άλλες ενότητες helpdesk ή άλλη διατύπωση των επικεφαλίδων υπάρχουν τα _merge_draft_get_source_tickets() και _merge_draft_section_name().
Δοκιμασμένο πριν τρέξει σε παραγωγικά δεδομένα
48 τεστ καλύπτουν μικτές καταστάσεις, διαφορετικούς συνεργάτες, νομίσματα και τύπους παραστατικών, γραμμές με και χωρίς παραγγελία πώλησης ή αγοράς, φόρους, ενότητες και σημειώσεις, κατανομές κέντρων κόστους, τη μεταφορά της καταγραφής χρόνου, και τις τρεις επιλογές για τα τιμολόγια πηγής, τον οδηγό και τις επικεφαλίδες – ότι είναι ενεργές από προεπιλογή, ότι η απενεργοποίησή τους αναπαράγει το παλαιό αποτέλεσμα μέχρι το τελευταίο λεπτό, ότι μια επικεφαλίδα δεν φέρει ποσά ή λογαριασμούς, ότι οι γραμμές μιας πηγής παραμένουν μαζί, ότι το ticket εμφανίζεται με τον αριθμό του και ποτέ με το ID της βάσης δεδομένων.
Τα δεδομένα δοκιμών βασίζονται στο υπάρχον λογιστικό σχέδιο της ενεργής εταιρείας αντί στο AccountTestInvoicingCommon – αυτό το βοηθητικό στοιχείο συχνά δεν ξεκινά σε μεταφερμένες παραγωγικές βάσεις δεδομένων, και ακριβώς σε τέτοιες θέλουμε να λειτουργεί η ενότητα.
odoo-bin -d <db> -u account_invoice_merge_draft --test-enable \
--test-tags /account_invoice_merge_draft --stop-after-init
Όπως με κάθε επέκταση στον λογιστικό τομέα, συνιστούμε να ελέγξετε την εγκατάσταση πρώτα σε δοκιμαστικό περιβάλλον με αντίγραφο της δικής σας βάσης δεδομένων.
Δωρεάν και ανοιχτού κώδικα για την κοινότητα του Odoo
Εμείς στη Biricon IT Services GmbH αναπτύσσουμε λύσεις αρχικά για συγκεκριμένες απαιτήσεις από τη δική μας λειτουργία. Όταν από αυτό προκύπτει μια ενότητα γενικής χρήσης, δεν θέλουμε να την κρύψουμε σε μία μόνο εγκατάσταση.
Το Merge Draft Invoices διατίθεται γι’ αυτό δωρεάν ως έργο ανοιχτού κώδικα. Ο πηγαίος κώδικας μπορεί να εξεταστεί, να ελεγχθεί, να προσαρμοστεί και να αναπτυχθεί περαιτέρω στο πλαίσιο της άδειας AGPL-3.
Στόχος μας δεν ήταν το συντομότερο δυνατό τιμολόγιο, αλλά ένα τιμολόγιο στο οποίο ακόμη και σε ένα χρόνο κάθε γραμμή οδηγεί πίσω στην παραγγελία της, στο ticket της και στην καταγραφή χρόνου της.
Έργο, πηγαίος κώδικας και συμμετοχή
Ο πλήρης πηγαίος κώδικας, οι οδηγίες εγκατάστασης και η τεχνική τεκμηρίωση είναι διαθέσιμα στο GitHub:
Άνοιγμα του Odoo 19 Merge Draft Invoices στο GitHub
Χαιρόμαστε για εμπειρίες από την πράξη, αναφορές σφαλμάτων, προτάσεις βελτίωσης και συνεισφορές από την κοινότητα του Odoo.
Δωρεάν. Κατανοητό. Ανοιχτού κώδικα. Για το Odoo 19 Community.