Όποιος τιμολογεί στο Odoo βάσει παραγγελιών, αιτημάτων υποστήριξης και καταγεγραμμένου χρόνου γνωρίζει το αποτέλεσμα: στο τέλος του μήνα, ο πελάτης λαμβάνει πέντε τιμολόγια αντί για ένα. Καθένα προέρχεται από διαφορετική παραγγελία ή αίτημα υποστήριξης και είναι απολύτως σωστό — κι όμως, ο πελάτης ρωτά αν θα μπορούσαν όλα να χωρέσουν σε ένα παραστατικό.
Ακριβώς γι’ αυτό το πρόβλημα, το Odoo 19 Community δεν διαθέτει μια ολοκληρωμένη λύση. Το γνωστό πρόσθετο account_invoice_merge της OCA δεν έχει κυκλοφορήσει ακόμη για το Odoo 19 και επιλύει το πρόβλημα με τρόπο που ενέχει σοβαρό κίνδυνο για τις επιχειρήσεις παροχής υπηρεσιών: οι όμοιες γραμμές ενοποιούνται, με αποτέλεσμα να χάνεται η σύνδεση με την παραγγελία, την εργασία και τον καταγεγραμμένο χρόνο.
Γι’ αυτό αναπτύξαμε το πρόσθετο Merge Draft Invoices για τη δική μας εγκατάσταση Odoo 19 Community. Επειδή το πρόβλημα αυτό δεν αφορά μόνο εμάς, το διαθέτουμε δωρεάν στο 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 επανασυνδέεται με το νέο τιμολόγιο |
| Ευανάγνωστο για τον πελάτη | τίποτα | μια επικεφαλίδα πάνω από τις γραμμές κάθε αρχικού τιμολογίου, με την παραγγελία και το αίτημα υποστήριξης |
| Αρχικά τιμολόγια | ακυρώνονται | ακύρωση (προεπιλογή), διαγραφή ή διατήρηση — με σαφή περιγραφή των συνεπειών |
Η ενοποίηση όμοιων γραμμών είναι η καθοριστική διαφορά. Μια ενοποιημένη γραμμή μπορεί να έχει μόνο μία προέλευση. Μόλις δύο γραμμές από δύο διαφορετικές παραγγελίες εμφανίζονται ίδιες, χάνεται η σύνδεση του τιμολογίου με την παραγγελία, την εργασία και την εγγραφή χρόνου. Σε μια επιχείρηση παροχής υπηρεσιών, όπου η τιμολόγηση ακολουθεί τη διαδρομή αίτημα υποστήριξης → καταγραφή χρόνου → τιμολόγιο, αυτό σημαίνει ότι οι ίδιες ώρες χρεώνονται δύο φορές ή χάνονται.
Γι’ αυτό το πρόσθετό μας δεν ενοποιεί ποτέ γραμμές. Το συγχωνευμένο τιμολόγιο έχει ακριβώς τόσες γραμμές όσες έχουν συνολικά τα αρχικά τιμολόγια.
Δεν χρησιμοποιήθηκε κώδικας από το πρόσθετο της 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) | επανασυνδέεται με το νέο τιμολόγιο πριν πάψουν να ισχύουν τα αρχικά |
| Αίτημα υποστήριξης | συνδέεται με την καταγραφή χρόνου και, επομένως, παραπέμπει αυτόματα στο νέο τιμολόγιο |
| Chatter | μήνυμα στο νέο τιμολόγιο με όλα τα αρχικά τιμολόγια και μήνυμα σε κάθε αρχικό με σύνδεσμο προς το νέο |
| merged_into_move_id / merged_from_move_ids | αποθηκευμένη αμφίδρομη σύνδεση |
Οι γραμμές φόρων, όρων πληρωμής, στρογγυλοποίησης και εκπτώσεων έγκαιρης εξόφλησης δεν αντιγράφονται, αλλά υπολογίζονται εκ νέου από το Odoo βάσει του περιεχομένου. Έτσι, το συγχωνευμένο τιμολόγιο έχει μία γραμμή φόρου ανά φορολογικό συντελεστή και όχι μία ανά αρχικό τιμολόγιο. Δημιουργείται ως πρόχειρο χωρίς αρίθμηση· η ίδια η συγχώνευση δεν δεσμεύει αριθμό παραστατικού.
Επικεφαλίδες: ο πελάτης βλέπει από πού προέρχεται κάθε γραμμή
Αυτή είναι η λειτουργία που κάνει τη διαφορά στην καθημερινή χρήση. Πάνω από τις γραμμές κάθε αρχικού τιμολογίου, το πρόσθετο εισάγει μια γραμμή ενότητας — το ίδιο στοιχείο που χρησιμοποιεί και το Odoo για την ενότητα «Προκαταβολές»:
BE10181 <- Επικεφαλίδα
22,500 x Ώρα τεχνικού 3.577,50
Μερικό σύνολο 3.577,50
BE10182 · Αίτημα: HT00219 <- Επικεφαλίδα
3,000 x Ώρα τεχνικού 477,00
Μερικό σύνολο 477,00
ΚΑΘΑΡΟ ΠΟΣΟ 4.054,50
Η επικεφαλίδα εκτυπώνεται στο παραστατικό που λαμβάνει ο πελάτης και η τυπική αναφορά τιμολογίου του Odoo προσθέτει αυτόματα ένα μερικό σύνολο ανά ενότητα. Έτσι, ο πελάτης βλέπει και πόσο κόστισε κάθε μέρος.
Το κείμενο της επικεφαλίδας συντίθεται από τα στοιχεία που διαθέτει πράγματι το αρχικό τιμολόγιο:
- τις παραγγελίες, απευθείας από τις συνδέσεις των γραμμών, ώστε οι ονομασίες να είναι ακριβείς· αν δεν υπάρχουν συνδέσεις με παραγγελίες, χρησιμοποιείται το ελεύθερο κείμενο του invoice_origin
- τα αιτήματα υποστήριξης, που εντοπίζονται μέσω της καταγραφής χρόνου και εμφανίζονται ως Αίτημα: HT00219 ή Αιτήματα: …, με όριο τα πέντε και την ένδειξη «και N ακόμη»
- αν δεν υπάρχει τίποτα από τα δύο: τον αριθμό του αρχικού τιμολογίου και, στη συνέχεια, την αναφορά του
- αν δεν υπάρχει κανένα στοιχείο: την ουδέτερη ένδειξη «Πρόσθετες γραμμές», ώστε οι γραμμές ενός αρχικού τιμολογίου να μην ενσωματώνονται ποτέ χωρίς ένδειξη στην προηγούμενη ενότητα
Ό,τι λείπει παραλείπεται, δεν επινοείται. Το αίτημα υποστήριξης εκτυπώνεται με τον δικό του αριθμό — τον αριθμό του 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.
- Δεν ενοποιούνται οι όμοιες γραμμές — σκόπιμα. Αν θέλετε ένα σύντομο, συνοπτικό τιμολόγιο, αυτό το πρόσθετο δεν σας εξυπηρετεί, καθώς κάτι τέτοιο θα σήμαινε απώλεια των συνδέσεων προέλευσης.
- Οι επικεφαλίδες δημιουργούνται μία φορά, κατά τη συγχώνευση. Στη συνέχεια είναι απλές γραμμές ενότητας: μπορούν να τροποποιηθούν ή να διαγραφούν και δεν ενημερώνονται αν αργότερα μετονομαστεί η παραγγελία ή το αίτημα υποστήριξης.
- Μια επικεφαλίδα αναφέρει τα αιτήματα υποστήριξης ολόκληρου του αρχικού τιμολογίου, όχι της μεμονωμένης γραμμής.
Εγκατάσταση στο Odoo 19 Community
Η εγκατάσταση γίνεται όπως για κάθε συνηθισμένο πρόσθετο του Odoo:
- Αντιγράψτε τον φάκελο του πρόσθετου σε μια διαδρομή που έχει οριστεί για τα πρόσθετα.
- Ενημερώστε τη λίστα εφαρμογών στο Odoo.
- Εγκαταστήστε το πρόσθετο account_invoice_merge_draft.
Το πρόσθετο εξαρτάται μόνο από το account. Όλες οι πληροφορίες που αξιοποιεί από τα sale, purchase, project, hr_timesheet, sale_timesheet ή κάποιο πρόσθετο υποστήριξης εντοπίζονται κατά την εκτέλεση — επομένως εγκαθίσταται και λειτουργεί ακόμη και σε μια βάση δεδομένων που χρησιμοποιείται αποκλειστικά για τιμολόγηση.
Στη συνέχεια: Λογιστική → Πελάτες → Τιμολόγια (ή Προμηθευτές → Τιμολόγια), επιλέξτε δύο ή περισσότερα πρόχειρα και κατόπιν μενού με το γρανάζι → Συγχώνευση πρόχειρων τιμολογίων. Η ενέργεια είναι διαθέσιμη σε όλους τους χρήστες που ανήκουν στην ομάδα Τιμολόγηση / Έκδοση τιμολογίων.
Αν κάνετε αναβάθμιση από προηγούμενη έκδοση του πρόσθετου, εκτελέστε μία φορά ενημέρωση με -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 account.move.line._copy_data_extend_business_fields του Odoo και το πεδίο μεταφέρεται εξίσου κατά τη συγχώνευση, την ακύρωση και τη δημιουργία αντιγράφου. Για άλλα πρόσθετα υποστήριξης ή για διαφορετική διατύπωση των επικεφαλίδων, υπάρχουν οι _merge_draft_get_source_tickets() και _merge_draft_section_name().
Δοκιμασμένο πριν χρησιμοποιηθεί σε δεδομένα παραγωγής
48 δοκιμές καλύπτουν συνδυασμούς διαφορετικών καταστάσεων, διαφορετικούς συναλλασσομένους, νομίσματα και τύπους παραστατικών, γραμμές με και χωρίς παραγγελία πώλησης ή αγοράς, φόρους, ενότητες και σημειώσεις, κατανομές σε κέντρα κόστους, την επανασύνδεση της καταγραφής χρόνου, και τις τρεις επιλογές για τα αρχικά τιμολόγια, τον οδηγό και τις επικεφαλίδες: ότι είναι ενεργές από προεπιλογή, ότι η απενεργοποίησή τους αναπαράγει το παλιό αποτέλεσμα μέχρι και το τελευταίο λεπτό, ότι μια επικεφαλίδα δεν περιέχει ποσά ή λογαριασμούς, ότι οι γραμμές κάθε αρχικού τιμολογίου παραμένουν μαζί και ότι το αίτημα υποστήριξης εμφανίζεται με τον αριθμό του και ποτέ με το 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.
Στόχος μας δεν ήταν το συντομότερο δυνατό τιμολόγιο, αλλά ένα τιμολόγιο στο οποίο, ακόμη και έπειτα από έναν χρόνο, κάθε γραμμή θα εξακολουθεί να συνδέεται με την παραγγελία, το αίτημα υποστήριξης και την εγγραφή χρόνου από όπου προήλθε.
Έργο, πηγαίος κώδικας και συνεισφορά
Ο πλήρης πηγαίος κώδικας, οι οδηγίες εγκατάστασης και η τεχνική τεκμηρίωση είναι διαθέσιμα στο GitHub:
Ανοίξτε το Odoo 19 Merge Draft Invoices στο GitHub
Χαιρόμαστε να λαμβάνουμε εμπειρίες από την πράξη, αναφορές σφαλμάτων, προτάσεις βελτίωσης και συνεισφορές από την κοινότητα του Odoo.
Δωρεάν. Κατανοητό. Ανοιχτού κώδικα. Για το Odoo 19 Community.