اگر در Odoo بر اساس سفارشها، تیکتهای پشتیبانی و زمانهای ثبتشده فاکتور صادر میکنید، این وضعیت برایتان آشناست: مشتری در پایان ماه بهجای یک فاکتور، پنج فاکتور دریافت میکند. هرکدام مربوط به سفارش یا تیکت متفاوتی است و از نظر کاری هم کاملاً درست است؛ بااینحال مشتری میپرسد آیا نمیشود همه را در یک سند آورد.
در Odoo 19 Community دقیقاً برای همین مسئله راهحل مناسبی وجود ندارد. ماژول شناختهشدهٔ OCA به نام account_invoice_merge هنوز برای Odoo 19 منتشر نشده است. ضمن اینکه روشی که برای حل مسئله به کار میبرد، برای شرکتهای خدماتی خطر جدی دارد: ردیفهای یکسان با هم تجمیع میشوند و در نتیجه، پیوندشان با سفارش، وظیفه و زمان ثبتشده از بین میرود.
به همین دلیل، برای سامانهٔ Odoo 19 Community خودمان ماژول Merge Draft Invoices را توسعه دادیم. چون این مشکل فقط مختص ما نیست، آن را با مجوز آزاد AGPL-3 بهرایگان در GitHub در دسترس قرار دادهایم.
پروژه در GitHub:
چرا بهجای راهحل OCA، ماژول خودمان را ساختیم؟
OCA سالهاست که نگهداری account_invoice_merge را در مخزن OCA/account-invoicing بر عهده دارد. در زمان نگارش این مطلب، انتقال آن به نسخهٔ 19.0 هنوز در حد یک درخواست ادغام باز است و ماژولی منتشر نشده است.
اما نکتهٔ مهمتر این است که این دو ماژول کارهای متفاوتی انجام میدهند:
| OCA account_invoice_merge | Merge Draft Invoices | |
|---|---|---|
| نتیجه | ادغام در یکی از فاکتورهای انتخابشده انجام میشود | همیشه یک فاکتور جدید |
| ردیفها | ردیفهای یکسان تجمیع و مقادیرشان جمع میشوند | هر ردیف عیناً و بهصورت مجزا منتقل میشود |
| مبدأ | مقادیر invoice_origin / payment_reference در سربرگ به هم متصل میشوند | فیلدهای مبدأ هر ردیف از طریق هوک داخلی Odoo به نام _copy_data_extend_business_fields کپی میشوند |
| ثبت زمان | در نظر گرفته نمیشود | timesheet_invoice_id به فاکتور جدید پیوند داده میشود |
| خوانا برای مشتری | هیچچیز | یک عنوان بالای ردیفهای هر فاکتور مبدأ، شامل سفارش و تیکت |
| فاکتورهای مبدأ | لغو میشوند | لغو (پیشفرض)، حذف یا نگهداری؛ با توضیح روشن پیامدهای هر گزینه |
تفاوت تعیینکننده، تجمیع ردیفهای یکسان است. یک ردیف تجمیعشده فقط میتواند یک مبدأ داشته باشد. بهمحض اینکه دو ردیف از دو سفارش متفاوت یکسان به نظر برسند، پیوند فاکتور با سفارش، وظیفه و رکورد ثبت زمان از بین میرود. در یک شرکت خدماتی که فرایند صدور فاکتور در آن بهصورت تیکت → ثبت زمان → فاکتور انجام میشود، این یعنی برای همان ساعتها دوبار صورتحساب صادر میشود یا آن ساعتها ناپدید میشوند.
به همین دلیل، ماژول ما اصولاً هیچ ردیفی را تجمیع نمیکند. تعداد ردیفهای فاکتور ادغامشده دقیقاً برابر با مجموع ردیفهای فاکتورهای مبدأ است.
هیچ کدی از ماژول OCA استفاده نشده است. هر دو ماژول تحت مجوز AGPL-3 هستند.
این ماژول چه میکند
در نمای فهرست فاکتورها، دو یا چند پیشنویس را انتخاب کنید و از منوی عملیات، ادغام فاکتورهای پیشنویس را بزنید. یک راهنما، فاکتورهای انتخابشده، بررسیها و گزینههای مربوط به فاکتورهای مبدأ را نشان میدهد. پس از تأیید، مستقیماً وارد فاکتور جدید میشوید.
ماژول پیش از ادغام، همهٔ موارد زیر را یکجا بررسی میکند تا مجبور نباشید پس از هر اصلاح، تازه با خطای بعدی روبهرو شوید:
- حداقل دو فاکتور انتخاب شده باشد
- همه در وضعیت پیشنویس باشند
- هیچکدام قبلاً ثبت حسابداری نشده باشند؛ سندی که به پیشنویس بازگردانده شده، سابقهٔ ثبت خود را حفظ میکند و پذیرفته نمیشود
- هیچکدام قبلاً ادغام نشده باشند
- همه از نوع فاکتور فروش، فاکتور تأمینکننده، سند بستانکار یا رسید باشند؛ نه صرفاً سند ثبت حسابداری
- طرف حساب، نوع سند، شرکت، ارز و دفتر روزنامه یکسان باشد
تفاوت در شرایط پرداخت، موقعیت مالیاتی، نشانی تحویل، حساب بانکی یا فروشنده مانع ادغام نمیشود. راهنما هشدار میدهد و مقدار مربوط به قدیمیترین فاکتور انتخابشده را به کار میبرد.
چه چیزهایی حفظ میشوند
اساس کار ماژول این است که هر پیوندی میان یک ردیف فاکتور و مبدأ آن، در فاکتور جدید نیز حفظ شود:
| پیوند | نحوهٔ پردازش |
|---|---|
| ردیفهای سفارش فروش (sale_line_ids) | برای هر ردیف، از طریق _copy_data_extend_business_fields در Odoo کپی میشوند |
| ردیفهای سفارش خرید (purchase_line_id) | به همین ترتیب |
| توزیع مراکز هزینه (analytic_distribution) | برای هر ردیف کپی میشود |
| سایر فیلدهای مبدأ در هر ردیف | بهصورت خودکار، بهشرط اینکه ماژول تأمینکنندهٔ آنها هوک مستندشدهٔ Odoo را پیادهسازی کرده باشد |
| مبدأ و مرجع در سربرگ فاکتور | ادغام میشوند، موارد تکراری حذف میشوند و در راهنما قابل ویرایش هستند |
| سفارش فروش: invoice_ids, invoice_status, qty_invoiced | از روی ردیفها محاسبه میشوند؛ سفارش بهطور خودکار فاکتور جدید را شناسایی میکند |
| ثبت زمان (timesheet_invoice_id) | پیش از نامعتبر شدن فاکتورهای مبدأ، به فاکتور جدید پیوند داده میشود |
| تیکت پشتیبانی | به ثبت زمان متصل است و در نتیجه، بهطور خودکار به فاکتور جدید ارجاع میدهد |
| گفتوگو | پیامی در فاکتور جدید با ذکر همهٔ فاکتورهای مبدأ و پیامی در هر فاکتور مبدأ با پیوند به فاکتور جدید |
| 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 یا نام نمایشی مدل، و هرگز با شناسهٔ پایگاه داده.
عنوانها به زبان مشتری نوشته میشوند؛ درست مانند کاری که Odoo هنگام ایجاد فاکتور از روی سفارش انجام میدهد. بخش گفتوگو به زبان کاربر باقی میماند.
عنوان هیچ محصول، مقدار، قیمت، مالیات، حساب یا توزیع مراکز هزینهای ندارد. بنابراین نمیتواند بر جمع مبالغ، مبنای مالیات یا حسابداری بهای تمامشده تأثیر بگذارد. اگر عنوانها را در راهنما غیرفعال کنید، دقیقاً همان فاکتوری ایجاد میشود که بدون این قابلیت ساخته میشد؛ یک آزمون، دو نتیجه را تا آخرین سنت با هم مقایسه میکند.
چه اتفاقی برای فاکتورهای مبدأ میافتد
راهنما سه گزینه ارائه میدهد:
- لغو (پیشفرض و پیشنهادی). لغو پیشنویس پیامد حسابداری ندارد: نه شمارهٔ سندی ایجاد میشود و نه ثبتی انجام میگیرد. فاکتورها بهصورت اسناد لغوشده، همراه با گفتوگوها و پیوند به فاکتور جدید، در پایگاه داده باقی میمانند. qty_invoiced در سفارش فروش، اسناد لغوشده را نادیده میگیرد و هیچ مقداری دوبار شمرده نمیشود. با این گزینه، حتی شش ماه بعد هم میتوان روند کار را پیگیری کرد.
- حذف. برگشتناپذیر است. گفتوگوها و پیوند بازگشت همراه با رکورد حذف میشوند؛ فقط پیام موجود در فاکتور جدید باقی میماند. برای پیشنویسهای موقتی مناسب است که دیگر نباید کسی آنها را ببیند.
- حفظ در وضعیت پیشنویس. فاکتورهای مبدأ باز میمانند. در این حالت، سفارشهای فروش مربوطه هم فاکتور مبدأ و هم فاکتور جدید را در محاسبهٔ مقادیر فاکتورشده لحاظ میکنند؛ بنابراین تا زمانی که کسی پیشنویسها را دستی سامان ندهد، به نظر میرسد برای کار انجامشده دوبار فاکتور صادر شده است. این گزینه برای مواقع نادری در نظر گرفته شده که میخواهید پیش از تصمیمگیری، مقایسه کنید. یک آزمون دقیقاً همین دوبارهشماری را ثبت میکند تا کسی غافلگیر نشود.
محدودیتها؛ کارهایی که ماژول عمداً انجام نمیدهد
لطفاً پیش از استفاده در محیط عملیاتی بخوانید:
- فقط پیشنویسها. فاکتورهایی که ثبت حسابداری شدهاند ادغام نمیشوند. رسیدگی به آنها به صدور سند بستانکار نیاز دارد، نه کپی کردن.
- پیشنویسهایی که قبلاً ثبت حسابداری شده و سپس به پیشنویس بازگردانده شدهاند، پذیرفته نمیشوند. این اسناد شماره و سابقهٔ ثبت دارند؛ پنهان کردن آنها در سندی دیگر کاری نیست که یک عملیات گروهی باید بیسروصدا انجام دهد.
- ادغام میان طرفحسابها، ارزها، شرکتها یا دفترهای روزنامهٔ متفاوت انجام نمیشود. برای این موارد نمیتوان یک مقدار واحد و معنادار تعیین کرد؛ بنابراین ماژول بهجای حدس زدن، درخواست را رد میکند.
- payment_reference منتقل نمیشود. شناسههای ساختاریافتهٔ پرداخت برای شناسایی یک سند هستند؛ چسباندن دو شناسه به هم، در بانک با چیزی مطابقت نخواهد داشت. فاکتور جدید هنگام ثبت حسابداری، شناسهٔ مخصوص خودش را میگیرد.
- ردیفهای پیشپرداخت مانند هر ردیف دیگری کپی میشوند. اگر فاکتورهای مبدأ شامل ردیفهای پیشپرداخت از sale باشند، باید نتیجه را پیش از ثبت حسابداری بررسی کنید.
- فقط پیوند timesheet_invoice_id بهطور خودکار به فاکتور جدید منتقل میشود. هر مدل دیگری که یک فیلد Many2one به account.move داشته باشد، مانند فرایندهای اختصاصی صدور فاکتور، پیوندهای اشتراک یا ارسالهای EDI، همچنان به فاکتور مبدأ اشاره خواهد کرد. هوک _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 و هوکها
عملیات ادغام را میتوانید از کد خودتان نیز فراخوانی کنید:
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}
)
اگر ارجاع شما روی ردیف فاکتور قرار دارد، اصلاً به این ماژول نیاز ندارید: هوک داخلی Odoo به نام account.move.line._copy_data_extend_business_fields کافی است و فیلد هنگام ادغام، لغو و ایجاد نسخهٔ تکراری به یک شکل منتقل میشود. برای پشتیبانی از ماژولهای پشتیبانی دیگر یا تغییر متن عنوانها نیز _merge_draft_get_source_tickets() و _merge_draft_section_name() در دسترساند.
آزمودهشده، پیش از اجرا روی دادههای عملیاتی
۴۸ آزمون این موارد را پوشش میدهند: وضعیتهای ترکیبی، طرفحسابها، ارزها و انواع سند متفاوت، ردیفهای دارای سفارش فروش یا خرید و بدون آنها، مالیاتها، بخشها و یادداشتها، توزیع مراکز هزینه، انتقال پیوند ثبت زمان، هر سه گزینهٔ مربوط به فاکتورهای مبدأ، راهنما و عنوانها. آزمونهای عنوانها بررسی میکنند که این قابلیت بهطور پیشفرض فعال باشد، غیرفعال کردنش نتیجهٔ قبلی را تا آخرین سنت بازتولید کند، عنوان هیچ مبلغ یا حسابی نداشته باشد، ردیفهای هر فاکتور مبدأ کنار هم بمانند و تیکت با شمارهٔ خودش نمایش داده شود، نه با شناسهٔ پایگاه داده.
دادههای آزمون بهجای 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.