هر که در Odoo از سفارشها، تیکتهای Helpdesk و ثبت زمان صورتحساب میکند، نتیجه را میشناسد: مشتری در پایان ماه یک فاکتور نمیگیرد، بلکه پنج فاکتور میگیرد. هر کدام از سفارش یا تیکت دیگری آمده و از نظر فنی همه درستاند – و با این حال مشتری میپرسد آیا اینها در یک سند جا نمیشود.
دقیقاً برای همین، در 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 در آنجا هنوز یک Pull Request باز است، نه یک ماژول منتشرشده.
اما مهمتر این است که این دو ماژول کارهای متفاوتی انجام میدهند:
| 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) | پیش از آنکه منابع نامعتبر شوند، به فاکتور جدید منتقل میشود |
| تیکت Helpdesk | به ثبت زمان وابسته است و به این ترتیب خودکار به فاکتور جدید اشاره میکند |
| Chatter | پیام در فاکتور جدید با همهی منابع، پیام در هر منبع با پیوند به فاکتور جدید |
| merged_into_move_id / merged_from_move_ids | پیوند ذخیرهشده در هر دو جهت |
ردیفهای مالیات، ردیفهای شرایط پرداخت، ردیفهای گردکردن و تخفیف نقدی (Skonto) کپی نمیشوند، بلکه 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 هنگام ساخت فاکتور از سفارشها انجام میدهد. Chatter به زبان کاربر میماند.
عنوان هیچ محصول، مقدار، قیمت، مالیات، حساب و توزیع مرکز هزینهای ندارد. نمیتواند بر جمعها، مبنای مالیاتی و حسابداری بهای تمامشده تأثیر بگذارد. هر که عنوانها را در دستیار خاموش کند، دقیقاً فاکتوری میگیرد که بدون این قابلیت پدید میآمد – یک آزمون هر دو نتیجه را تا سنت مقایسه میکند.
چه بر سر فاکتورهای منبع میآید
دستیار سه گزینه ارائه میدهد:
- ابطال (پیشفرض، توصیهشده). ابطال یک پیشنویس بیپیامد است: بدون شمارهی سند، بدون ثبت. فاکتورها بهصورت اسناد ابطالشده با Chatter و پیوند به فاکتور جدید در پایگاه داده میمانند. qty_invoiced در سفارش فروش اسناد ابطالشده را نادیده میگیرد، هیچ چیز دو بار شمرده نمیشود. این گزینهای است که پس از شش ماه هم قابلپیگیری است.
- حذف. بازگشتناپذیر. Chatter و ارجاع بازگشتی همراه با رکورد از میان میروند؛ فقط پیام در فاکتور جدید میماند. مناسب پیشنویسهای دورریختنی که دیگر نباید کسی ببیند.
- نگهداشتن بهصورت پیشنویس. منابع باز میمانند. سفارشهای فروش آنها آنگاه منبع و فاکتور جدید را هر دو فاکتورشده میشمارند – کار دوبار صورتحسابشده به نظر میرسد تا زمانی که کسی پیشنویسها را دستی پاکسازی کند. برای موارد نادری در نظر گرفته شده که کسی میخواهد پیش از تصمیم مقایسه کند. یک آزمون دقیقاً همین شمارش مضاعف را ثبت میکند تا کسی را غافلگیر نکند.
محدودیتها – آنچه ماژول عمداً انجام نمیدهد
لطفاً پیش از استفادهی عملیاتی بخوانید:
- فقط پیشنویسها. فاکتورهای ثبتشده ادغام نمیشوند. این یک کار یادداشت بستانکار است، نه یک عملیات کپی.
- پیشنویسهایی که یک بار ثبت و سپس بازگردانده شدهاند رد میشوند. آنها شماره و تاریخچهی ثبت دارند؛ پنهانکردن این در سندی دیگر چیزی نیست که یک عملیات گروهی باید بیسروصدا انجام دهد.
- ادغام در سطح شرکای تجاری، ارزها، شرکتها یا دفترهای روزنامه ممکن نیست. برای این کار مقدار واحد معناداری وجود ندارد، بنابراین ماژول بهجای حدس زدن، رد میکند.
- payment_reference منتقل نمیشود. ارجاعهای پرداخت ساختاریافته یک سند را شناسایی میکنند؛ دو ارجاعِ به هم چسبیده در بانک با هیچ چیز مطابقت نمیکنند. فاکتور جدید هنگام ثبت، ارجاع خودش را میگیرد.
- اقلام پیشپرداخت مانند هر قلم دیگری کپی میشوند. اگر منابع ردیفهای پیشپرداخت از sale داشته باشند، نتیجه باید پیش از ثبت بررسی شود.
- فقط timesheet_invoice_id بهطور خودکار منتقل میشود. هر مدل دیگری که یک Many2one به account.move دارد (اجراهای صورتحساب اختصاصی، پیوندهای اشتراک، ارسالهای EDI) همچنان به منبع اشاره میکند. برای این منظور هوک _merge_draft_relink وجود دارد.
- هیچ ادغامی از اقلام یکسان انجام نمیشود – عمدی. هر که فاکتور کوتاه و مرتب میخواهد در اینجا جای اشتباهی آمده و بهایش را با پیوندهای منشأ میپردازد.
- عنوانها فقط یکبار هنگام ادغام ایجاد میشوند. پس از آن ردیفهای بخش معمولی هستند: قابل ویرایش، قابل حذف و در صورت تغییر نام بعدی سفارش یا تیکت بهروز نمیشوند.
- عنوان، تیکتهای کل فاکتور منبع را نام میبرد، نه تیکتهای هر قلم.
نصب در 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 و هوکها
ادغام را میتوان از کد خودتان نیز فراخوانی کرد:
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() همان دلایل را بهصورت فهرست و بدون Exception برمیگرداند.
هر که ارجاعهای بازگشتی اختصاصی به 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 کافی است و فیلد هنگام ادغام، ابطال و تکثیر به یک شکل منتقل میشود. برای ماژولهای Helpdesk دیگر یا عبارتبندی متفاوت عنوانها، _merge_draft_get_source_tickets() و _merge_draft_section_name() وجود دارند.
پیش از اجرا روی دادههای عملیاتی آزمایش شده است
48 آزمون این موارد را پوشش میدهند: وضعیتهای ترکیبی، شرکای تجاری، ارزها و انواع سند متفاوت، اقلام با و بدون سفارش فروش یا خرید، مالیاتها، بخشها و یادداشتها، توزیع مراکز هزینه، انتقال ثبت زمان، هر سه گزینهی فاکتورهای منبع، دستیار و عنوانها – اینکه بهطور پیشفرض فعالاند، اینکه خاموشکردنشان نتیجهی قدیمی را تا سنت بازتولید میکند، اینکه عنوان هیچ مبلغ یا حسابی ندارد، اینکه اقلام یک منبع کنار هم میمانند، اینکه تیکت با شمارهی خودش و هرگز با شناسهی پایگاه داده ظاهر میشود.
دادههای آزمون بر پایهی نمودار حسابهای موجود شرکت فعال ساخته شدهاند، نه بر AccountTestInvoicingCommon – این Helper روی پایگاههای دادهی عملیاتی مهاجرتیافته اغلب راه نمیافتد، و ماژول دقیقاً باید روی چنین پایگاههایی اجرا شود.
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.