From e8331d40f3ba1f7d31f7f0cd03b03d1c4b761af9 Mon Sep 17 00:00:00 2001 From: ankitjavalkarwork Date: Mon, 25 Aug 2014 18:00:12 +0530 Subject: [PATCH] Commonify Recurring Sales Order/Invoice --- .../doctype/sales_invoice/sales_invoice.js | 14 +- .../doctype/sales_invoice/sales_invoice.json | 23 +- .../doctype/sales_invoice/sales_invoice.py | 252 +++++++++--------- .../sales_invoice/test_sales_invoice.py | 36 +-- erpnext/controllers/accounts_controller.py | 51 ++++ erpnext/controllers/recurring_document.py | 121 +++++++++ .../doctype/sales_order/sales_order.json | 131 ++++++++- .../doctype/sales_order/sales_order.py | 8 + .../emails/recurring_invoice_failed.html | 14 +- 9 files changed, 479 insertions(+), 171 deletions(-) create mode 100644 erpnext/controllers/recurring_document.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 76092ed30d..5228b0e383 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -228,7 +228,7 @@ cur_frm.cscript.hide_fields = function(doc) { par_flds = ['project_name', 'due_date', 'is_opening', 'source', 'total_advance', 'gross_profit', 'gross_profit_percent', 'get_advances_received', 'advance_adjustment_details', 'sales_partner', 'commission_rate', - 'total_commission', 'advances', 'invoice_period_from_date', 'invoice_period_to_date']; + 'total_commission', 'advances', 'period_from', 'period_to']; item_flds_normal = ['sales_order', 'delivery_note'] @@ -414,18 +414,18 @@ cur_frm.cscript.convert_into_recurring_invoice = function(doc, dt, dn) { refresh_many(["notification_email_address", "repeat_on_day_of_month"]); } -cur_frm.cscript.invoice_period_from_date = function(doc, dt, dn) { - // set invoice_period_to_date - if(doc.invoice_period_from_date) { +cur_frm.cscript.period_from = function(doc, dt, dn) { + // set period_to + if(doc.period_from) { var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}; var months = recurring_type_map[doc.recurring_type]; if(months) { - var to_date = frappe.datetime.add_months(doc.invoice_period_from_date, + var to_date = frappe.datetime.add_months(doc.period_from, months); - doc.invoice_period_to_date = frappe.datetime.add_days(to_date, -1); - refresh_field('invoice_period_to_date'); + doc.period_to = frappe.datetime.add_days(to_date, -1); + refresh_field('period_to'); } } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index ff256dc777..7cab4c24f0 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,5 +1,6 @@ { - "allow_import": 1, + "allow_attach": 1, + "allow_import": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", "default_print_format": "Standard", @@ -172,7 +173,7 @@ "allow_on_submit": 1, "depends_on": "", "description": "Start date of current invoice's period", - "fieldname": "invoice_period_from_date", + "fieldname": "period_from", "fieldtype": "Date", "label": "Invoice Period From", "no_copy": 1, @@ -184,7 +185,7 @@ "allow_on_submit": 1, "depends_on": "", "description": "End date of current invoice's period", - "fieldname": "invoice_period_to_date", + "fieldname": "period_to", "fieldtype": "Date", "label": "Invoice Period To", "no_copy": 1, @@ -1087,7 +1088,7 @@ "allow_on_submit": 1, "depends_on": "eval:doc.docstatus<2", "description": "Check if recurring invoice, uncheck to stop recurring or put proper End Date", - "fieldname": "convert_into_recurring_invoice", + "fieldname": "convert_into_recurring", "fieldtype": "Check", "label": "Convert into Recurring Invoice", "no_copy": 1, @@ -1097,7 +1098,7 @@ }, { "allow_on_submit": 1, - "depends_on": "eval:doc.convert_into_recurring_invoice==1", + "depends_on": "eval:doc.convert_into_recurring==1", "description": "Select the period when the invoice will be generated automatically", "fieldname": "recurring_type", "fieldtype": "Select", @@ -1110,7 +1111,7 @@ }, { "allow_on_submit": 1, - "depends_on": "eval:doc.convert_into_recurring_invoice==1", + "depends_on": "eval:doc.convert_into_recurring==1", "description": "The day of the month on which auto invoice will be generated e.g. 05, 28 etc ", "fieldname": "repeat_on_day_of_month", "fieldtype": "Int", @@ -1121,7 +1122,7 @@ "read_only": 0 }, { - "depends_on": "eval:doc.convert_into_recurring_invoice==1", + "depends_on": "eval:doc.convert_into_recurring==1", "description": "The date on which next invoice will be generated. It is generated on submit.\n", "fieldname": "next_date", "fieldtype": "Date", @@ -1133,7 +1134,7 @@ }, { "allow_on_submit": 1, - "depends_on": "eval:doc.convert_into_recurring_invoice==1", + "depends_on": "eval:doc.convert_into_recurring==1", "description": "The date on which recurring invoice will be stop", "fieldname": "end_date", "fieldtype": "Date", @@ -1153,7 +1154,7 @@ "width": "50%" }, { - "depends_on": "eval:doc.convert_into_recurring_invoice==1", + "depends_on": "eval:doc.convert_into_recurring==1", "description": "The unique id for tracking all recurring invoices.\u00a0It is generated on submit.", "fieldname": "recurring_id", "fieldtype": "Data", @@ -1165,7 +1166,7 @@ }, { "allow_on_submit": 1, - "depends_on": "eval:doc.convert_into_recurring_invoice==1", + "depends_on": "eval:doc.convert_into_recurring==1", "description": "Enter email id separated by commas, invoice will be mailed automatically on particular date", "fieldname": "notification_email_address", "fieldtype": "Small Text", @@ -1192,7 +1193,7 @@ "icon": "icon-file-text", "idx": 1, "is_submittable": 1, - "modified": "2014-08-14 02:13:09.673510", + "modified": "2014-08-25 17:41:35.367233", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 481ae098b3..69a7def900 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -75,7 +75,7 @@ class SalesInvoice(SellingController): self.set_against_income_account() self.validate_c_form() self.validate_time_logs_are_submitted() - self.validate_recurring_invoice() + self.validate_recurring_document() self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "delivery_note_details") @@ -103,7 +103,7 @@ class SalesInvoice(SellingController): self.update_c_form() self.update_time_log_batch(self.name) - self.convert_to_recurring() + self.convert_to_recurring("RECINV.#####", self.transaction_date) def before_cancel(self): self.update_time_log_batch(None) @@ -144,8 +144,8 @@ class SalesInvoice(SellingController): }) def on_update_after_submit(self): - self.validate_recurring_invoice() - self.convert_to_recurring() + self.validate_recurring_document() + self.convert_to_recurring("RECINV.#####", self.transaction_date) def get_portal_page(self): return "invoice" if self.docstatus==1 else None @@ -592,157 +592,157 @@ class SalesInvoice(SellingController): grand_total = %s where invoice_no = %s and parent = %s""", (self.name, self.amended_from, self.c_form_no)) - def validate_recurring_invoice(self): - if self.convert_into_recurring_invoice: - self.validate_notification_email_id() +# def validate_recurring_invoice(self): +# if self.convert_into_recurring_invoice: +# self.validate_notification_email_id() - if not self.recurring_type: - msgprint(_("Please select {0}").format(self.meta.get_label("recurring_type")), - raise_exception=1) +# if not self.recurring_type: +# msgprint(_("Please select {0}").format(self.meta.get_label("recurring_type")), +# raise_exception=1) - elif not (self.invoice_period_from_date and \ - self.invoice_period_to_date): - throw(_("Invoice Period From and Invoice Period To dates mandatory for recurring invoice")) +# elif not (self.period_from and \ +# self.period_to): +# throw(_("Invoice Period From and Invoice Period To dates mandatory for recurring invoice")) - def convert_to_recurring(self): - if self.convert_into_recurring_invoice: - if not self.recurring_id: - frappe.db.set(self, "recurring_id", - make_autoname("RECINV/.#####")) +# def convert_to_recurring(self): +# if self.convert_into_recurring_invoice: +# if not self.recurring_id: +# frappe.db.set(self, "recurring_id", +# make_autoname("RECINV/.#####")) - self.set_next_date() +# self.set_next_date() - elif self.recurring_id: - frappe.db.sql("""update `tabSales Invoice` - set convert_into_recurring_invoice = 0 - where recurring_id = %s""", (self.recurring_id,)) +# elif self.recurring_id: +# frappe.db.sql("""update `tabSales Invoice` +# set convert_into_recurring_invoice = 0 +# where recurring_id = %s""", (self.recurring_id,)) - def validate_notification_email_id(self): - if self.notification_email_address: - email_list = filter(None, [cstr(email).strip() for email in - self.notification_email_address.replace("\n", "").split(",")]) +# def validate_notification_email_id(self): +# if self.notification_email_address: +# email_list = filter(None, [cstr(email).strip() for email in +# self.notification_email_address.replace("\n", "").split(",")]) - from frappe.utils import validate_email_add - for email in email_list: - if not validate_email_add(email): - throw(_("{0} is an invalid email address in 'Notification Email Address'").format(email)) +# from frappe.utils import validate_email_add +# for email in email_list: +# if not validate_email_add(email): +# throw(_("{0} is an invalid email address in 'Notification Email Address'").format(email)) - else: - throw(_("'Notification Email Addresses' not specified for recurring invoice")) +# else: +# throw(_("'Notification Email Addresses' not specified for recurring invoice")) - def set_next_date(self): - """ Set next date on which auto invoice will be created""" - if not self.repeat_on_day_of_month: - msgprint(_("Please enter 'Repeat on Day of Month' field value"), raise_exception=1) +# def set_next_date(self): +# """ Set next date on which auto invoice will be created""" +# if not self.repeat_on_day_of_month: +# msgprint(_("Please enter 'Repeat on Day of Month' field value"), raise_exception=1) - next_date = get_next_date(self.posting_date, - month_map[self.recurring_type], cint(self.repeat_on_day_of_month)) +# next_date = get_next_date(self.posting_date, +# month_map[self.recurring_type], cint(self.repeat_on_day_of_month)) - frappe.db.set(self, 'next_date', next_date) +# frappe.db.set(self, 'next_date', next_date) -def get_next_date(dt, mcount, day=None): - dt = getdate(dt) +# def get_next_date(dt, mcount, day=None): +# dt = getdate(dt) - from dateutil.relativedelta import relativedelta - dt += relativedelta(months=mcount, day=day) +# from dateutil.relativedelta import relativedelta +# dt += relativedelta(months=mcount, day=day) - return dt +# return dt -def manage_recurring_invoices(next_date=None, commit=True): - """ - Create recurring invoices on specific date by copying the original one - and notify the concerned people - """ - next_date = next_date or nowdate() - recurring_invoices = frappe.db.sql("""select name, recurring_id - from `tabSales Invoice` where ifnull(convert_into_recurring_invoice, 0)=1 - and docstatus=1 and next_date=%s - and next_date <= ifnull(end_date, '2199-12-31')""", next_date) +# def manage_recurring_invoices(next_date=None, commit=True): +# """ +# Create recurring invoices on specific date by copying the original one +# and notify the concerned people +# """ +# next_date = next_date or nowdate() +# recurring_invoices = frappe.db.sql("""select name, recurring_id +# from `tabSales Invoice` where ifnull(convert_into_recurring_invoice, 0)=1 +# and docstatus=1 and next_date=%s +# and next_date <= ifnull(end_date, '2199-12-31')""", next_date) - exception_list = [] - for ref_invoice, recurring_id in recurring_invoices: - if not frappe.db.sql("""select name from `tabSales Invoice` - where posting_date=%s and recurring_id=%s and docstatus=1""", - (next_date, recurring_id)): - try: - ref_wrapper = frappe.get_doc('Sales Invoice', ref_invoice) - new_invoice_wrapper = make_new_invoice(ref_wrapper, next_date) - send_notification(new_invoice_wrapper) - if commit: - frappe.db.commit() - except: - if commit: - frappe.db.rollback() +# exception_list = [] +# for ref_invoice, recurring_id in recurring_invoices: +# if not frappe.db.sql("""select name from `tabSales Invoice` +# where posting_date=%s and recurring_id=%s and docstatus=1""", +# (next_date, recurring_id)): +# try: +# ref_wrapper = frappe.get_doc('Sales Invoice', ref_invoice) +# new_invoice_wrapper = make_new_invoice(ref_wrapper, next_date) +# send_notification(new_invoice_wrapper) +# if commit: +# frappe.db.commit() +# except: +# if commit: +# frappe.db.rollback() - frappe.db.begin() - frappe.db.sql("update `tabSales Invoice` set \ - convert_into_recurring_invoice = 0 where name = %s", ref_invoice) - notify_errors(ref_invoice, ref_wrapper.customer, ref_wrapper.owner) - frappe.db.commit() +# frappe.db.begin() +# frappe.db.sql("update `tabSales Invoice` set \ +# convert_into_recurring_invoice = 0 where name = %s", ref_invoice) +# notify_errors(ref_invoice, ref_wrapper.customer, ref_wrapper.owner) +# frappe.db.commit() - exception_list.append(frappe.get_traceback()) - finally: - if commit: - frappe.db.begin() +# exception_list.append(frappe.get_traceback()) +# finally: +# if commit: +# frappe.db.begin() - if exception_list: - exception_message = "\n\n".join([cstr(d) for d in exception_list]) - frappe.throw(exception_message) +# if exception_list: +# exception_message = "\n\n".join([cstr(d) for d in exception_list]) +# frappe.throw(exception_message) -def make_new_invoice(ref_wrapper, posting_date): - from erpnext.accounts.utils import get_fiscal_year - new_invoice = frappe.copy_doc(ref_wrapper) +# def make_new_invoice(ref_wrapper, posting_date): +# from erpnext.accounts.utils import get_fiscal_year +# new_invoice = frappe.copy_doc(ref_wrapper) - mcount = month_map[ref_wrapper.recurring_type] +# mcount = month_map[ref_wrapper.recurring_type] - invoice_period_from_date = get_next_date(ref_wrapper.invoice_period_from_date, mcount) +# period_from = get_next_date(ref_wrapper.period_from, mcount) - # get last day of the month to maintain period if the from date is first day of its own month - # and to date is the last day of its own month - if (cstr(get_first_day(ref_wrapper.invoice_period_from_date)) == \ - cstr(ref_wrapper.invoice_period_from_date)) and \ - (cstr(get_last_day(ref_wrapper.invoice_period_to_date)) == \ - cstr(ref_wrapper.invoice_period_to_date)): - invoice_period_to_date = get_last_day(get_next_date(ref_wrapper.invoice_period_to_date, - mcount)) - else: - invoice_period_to_date = get_next_date(ref_wrapper.invoice_period_to_date, mcount) +# # get last day of the month to maintain period if the from date is first day of its own month +# # and to date is the last day of its own month +# if (cstr(get_first_day(ref_wrapper.period_from)) == \ +# cstr(ref_wrapper.period_from)) and \ +# (cstr(get_last_day(ref_wrapper.period_to)) == \ +# cstr(ref_wrapper.period_to)): +# period_to = get_last_day(get_next_date(ref_wrapper.period_to, +# mcount)) +# else: +# period_to = get_next_date(ref_wrapper.period_to, mcount) - new_invoice.update({ - "posting_date": posting_date, - "aging_date": posting_date, - "due_date": add_days(posting_date, cint(date_diff(ref_wrapper.due_date, - ref_wrapper.posting_date))), - "invoice_period_from_date": invoice_period_from_date, - "invoice_period_to_date": invoice_period_to_date, - "fiscal_year": get_fiscal_year(posting_date)[0], - "owner": ref_wrapper.owner, - }) +# new_invoice.update({ +# "posting_date": posting_date, +# "aging_date": posting_date, +# "due_date": add_days(posting_date, cint(date_diff(ref_wrapper.due_date, +# ref_wrapper.posting_date))), +# "period_from": period_from, +# "period_to": period_to, +# "fiscal_year": get_fiscal_year(posting_date)[0], +# "owner": ref_wrapper.owner, +# }) - new_invoice.submit() +# new_invoice.submit() - return new_invoice +# return new_invoice -def send_notification(new_rv): - """Notify concerned persons about recurring invoice generation""" - frappe.sendmail(new_rv.notification_email_address, - subject="New Invoice : " + new_rv.name, - message = _("Please find attached Sales Invoice #{0}").format(new_rv.name), - attachments = [{ - "fname": new_rv.name + ".pdf", - "fcontent": frappe.get_print_format(new_rv.doctype, new_rv.name, as_pdf=True) - }]) +# def send_notification(new_rv): +# """Notify concerned persons about recurring invoice generation""" +# frappe.sendmail(new_rv.notification_email_address, +# subject="New Invoice : " + new_rv.name, +# message = _("Please find attached Sales Invoice #{0}").format(new_rv.name), +# attachments = [{ +# "fname": new_rv.name + ".pdf", +# "fcontent": frappe.get_print_format(new_rv.doctype, new_rv.name, as_pdf=True) +# }]) -def notify_errors(inv, customer, owner): - from frappe.utils.user import get_system_managers - recipients=get_system_managers(only_name=True) +# def notify_errors(inv, customer, owner): +# from frappe.utils.user import get_system_managers +# recipients=get_system_managers(only_name=True) - frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], - subject="[Urgent] Error while creating recurring invoice for %s" % inv, - message = frappe.get_template("templates/emails/recurring_invoice_failed.html").render({ - "name": inv, - "customer": customer - })) +# frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], +# subject="[Urgent] Error while creating recurring invoice for %s" % inv, +# message = frappe.get_template("templates/emails/recurring_invoice_failed.html").render({ +# "name": inv, +# "customer": customer +# })) assign_task_to_owner(inv, "Recurring Invoice Failed", recipients) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ab361d83ad..44bd451ed8 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -677,8 +677,8 @@ class TestSalesInvoice(unittest.TestCase): "posting_date": today, "due_date": None, "fiscal_year": get_fiscal_year(today)[0], - "invoice_period_from_date": get_first_day(today), - "invoice_period_to_date": get_last_day(today) + "period_from": get_first_day(today), + "period_to": get_last_day(today) }) # monthly @@ -690,8 +690,8 @@ class TestSalesInvoice(unittest.TestCase): # monthly without a first and last day period si2 = frappe.copy_doc(base_si) si2.update({ - "invoice_period_from_date": today, - "invoice_period_to_date": add_to_date(today, days=30) + "period_from": today, + "period_to": add_to_date(today, days=30) }) si2.insert() si2.submit() @@ -701,8 +701,8 @@ class TestSalesInvoice(unittest.TestCase): si3 = frappe.copy_doc(base_si) si3.update({ "recurring_type": "Quarterly", - "invoice_period_from_date": get_first_day(today), - "invoice_period_to_date": get_last_day(add_to_date(today, months=3)) + "period_from": get_first_day(today), + "period_to": get_last_day(add_to_date(today, months=3)) }) si3.insert() si3.submit() @@ -712,8 +712,8 @@ class TestSalesInvoice(unittest.TestCase): si4 = frappe.copy_doc(base_si) si4.update({ "recurring_type": "Quarterly", - "invoice_period_from_date": today, - "invoice_period_to_date": add_to_date(today, months=3) + "period_from": today, + "period_to": add_to_date(today, months=3) }) si4.insert() si4.submit() @@ -723,8 +723,8 @@ class TestSalesInvoice(unittest.TestCase): si5 = frappe.copy_doc(base_si) si5.update({ "recurring_type": "Yearly", - "invoice_period_from_date": get_first_day(today), - "invoice_period_to_date": get_last_day(add_to_date(today, years=1)) + "period_from": get_first_day(today), + "period_to": get_last_day(add_to_date(today, years=1)) }) si5.insert() si5.submit() @@ -734,8 +734,8 @@ class TestSalesInvoice(unittest.TestCase): si6 = frappe.copy_doc(base_si) si6.update({ "recurring_type": "Yearly", - "invoice_period_from_date": today, - "invoice_period_to_date": add_to_date(today, years=1) + "period_from": today, + "period_to": add_to_date(today, years=1) }) si6.insert() si6.submit() @@ -784,16 +784,16 @@ class TestSalesInvoice(unittest.TestCase): self.assertEquals(new_si.posting_date, unicode(next_date)) - self.assertEquals(new_si.invoice_period_from_date, - unicode(add_months(base_si.invoice_period_from_date, no_of_months))) + self.assertEquals(new_si.period_from, + unicode(add_months(base_si.period_from, no_of_months))) if first_and_last_day: - self.assertEquals(new_si.invoice_period_to_date, - unicode(get_last_day(add_months(base_si.invoice_period_to_date, + self.assertEquals(new_si.period_to, + unicode(get_last_day(add_months(base_si.period_to, no_of_months)))) else: - self.assertEquals(new_si.invoice_period_to_date, - unicode(add_months(base_si.invoice_period_to_date, no_of_months))) + self.assertEquals(new_si.period_to, + unicode(add_months(base_si.period_to, no_of_months))) return new_si diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 59a49afb22..9aa93ac8a0 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -444,6 +444,57 @@ class AccountsController(TransactionBase): if total_outstanding: frappe.get_doc('Account', account).check_credit_limit(total_outstanding) + def validate_recurring_document(self): + if self.convert_into_recurring: + self.validate_notification_email_id() + + if not self.recurring_type: + msgprint(_("Please select {0}").format(self.meta.get_label("recurring_type")), + raise_exception=1) + + elif not (self.period_from and self.period_to): + throw(_("Period From and Period To dates mandatory for recurring %s") % self.doctype) + + def convert_to_recurring(self, autoname, posting_date): + if self.convert_into_recurring: + if not self.recurring_id: + frappe.db.set(self, "recurring_id", + make_autoname(autoname)) + + self.set_next_date(posting_date) + + elif self.recurring_id: + frappe.db.sql("""update `tab%s` \ + set convert_into_recurring = 0 \ + where recurring_id = %s""", % (self.doctype, '%s'), (self.recurring_id)) + + def validate_notification_email_id(self): + if self.notification_email_address: + email_list = filter(None, [cstr(email).strip() for email in + self.notification_email_address.replace("\n", "").split(",")]) + + from frappe.utils import validate_email_add + for email in email_list: + if not validate_email_add(email): + throw(_("{0} is an invalid email address in 'Notification \ + Email Address'").format(email)) + + else: + frappe.throw(_("'Notification Email Addresses' not specified for recurring %s") \ + % self.doctype) + + def set_next_date(self, posting_date): + """ Set next date on which recurring document will be created""" + from erpnext.controllers.recurring_document import get_next_date + + if not self.repeat_on_day_of_month: + msgprint(_("Please enter 'Repeat on Day of Month' field value"), raise_exception=1) + + next_date = get_next_date(posting_date, month_map[self.recurring_type], + cint(self.repeat_on_day_of_month)) + + frappe.db.set(self, 'next_date', next_date) + @frappe.whitelist() def get_tax_rate(account_head): diff --git a/erpnext/controllers/recurring_document.py b/erpnext/controllers/recurring_document.py new file mode 100644 index 0000000000..ad32371b86 --- /dev/null +++ b/erpnext/controllers/recurring_document.py @@ -0,0 +1,121 @@ +from __future__ import unicode_literals +import frappe +import frappe.utils +import frappe.defaults + +from frappe.utils import add_days, cint, cstr, date_diff, flt, getdate, nowdate, \ + get_first_day, get_last_day, comma_and +from frappe.model.naming import make_autoname + +from frappe import _, msgprint, throw +from erpnext.accounts.party import get_party_account, get_due_date, get_party_details +from frappe.model.mapper import get_mapped_doc + +def manage_recurring_documents(doctype, next_date=None, commit=True): + """ + Create recurring documents on specific date by copying the original one + and notify the concerned people + """ + next_date = next_date or nowdate() + recurring_documents = frappe.db.sql("""select name, recurring_id + from `tab%s` where ifnull(convert_into_recurring, 0)=1 + and docstatus=1 and next_date=%s + and next_date <= ifnull(end_date, '2199-12-31')""", % (doctype, '%s'), (next_date)) + + exception_list = [] + for ref_document, recurring_id in recurring_documents: + if not frappe.db.sql("""select name from `tab%s` + where transaction_date=%s and recurring_id=%s and docstatus=1""", + % (doctype, '%s', '%s'), (next_date, recurring_id)): + try: + ref_wrapper = frappe.get_doc(doctype, ref_document) + new_document_wrapper = make_new_document(ref_wrapper, next_date) + send_notification(new_document_wrapper) + if commit: + frappe.db.commit() + except: + if commit: + frappe.db.rollback() + + frappe.db.begin() + frappe.db.sql("update `tab%s` \ + set convert_into_recurring = 0 where name = %s", % (doctype, '%s'), + (ref_document)) + notify_errors(ref_document, doctype, ref_wrapper.customer, ref_wrapper.owner) + frappe.db.commit() + + exception_list.append(frappe.get_traceback()) + finally: + if commit: + frappe.db.begin() + + if exception_list: + exception_message = "\n\n".join([cstr(d) for d in exception_list]) + frappe.throw(exception_message) + +def make_new_document(ref_wrapper, posting_date): + from erpnext.accounts.utils import get_fiscal_year + new_document = frappe.copy_doc(ref_wrapper) + + mcount = month_map[ref_wrapper.recurring_type] + + period_from = get_next_date(ref_wrapper.period_from, mcount) + + # get last day of the month to maintain period if the from date is first day of its own month + # and to date is the last day of its own month + if (cstr(get_first_day(ref_wrapper.period_from)) == \ + cstr(ref_wrapper.period_from)) and \ + (cstr(get_last_day(ref_wrapper.period_to)) == \ + cstr(ref_wrapper.period_to)): + period_to = get_last_day(get_next_date(ref_wrapper.period_to, + mcount)) + else: + period_to = get_next_date(ref_wrapper.period_to, mcount) + + new_document.update({ + "transaction_date": posting_date, + "period_from": period_from, + "period_to": period_to, + "fiscal_year": get_fiscal_year(posting_date)[0], + "owner": ref_wrapper.owner, + }) + + if ref_wrapper.doctype == "Sales Order": + new_document.update({ + "delivery_date": get_next_date(ref_wrapper.delivery_date, mcount, + cint(ref_wrapper.repeat_on_day_of_month)) + }) + + new_document.submit() + + return new_document + +def get_next_date(dt, mcount, day=None): + dt = getdate(dt) + + from dateutil.relativedelta import relativedelta + dt += relativedelta(months=mcount, day=day) + + return dt + +def send_notification(new_rv): + """Notify concerned persons about recurring document generation""" + frappe.sendmail(new_rv.notification_email_address, + subject= _("New {0}: #{1}").format(new_rv.doctype, new_rv.name), + message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name), + attachments = [{ + "fname": new_rv.name + ".pdf", + "fcontent": frappe.get_print_format(new_rv.doctype, new_rv.name, as_pdf=True) + }]) + +def notify_errors(doc, doctype, customer, owner): + from frappe.utils.user import get_system_managers + recipients = get_system_managers(only_name=True) + + frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], + subject="[Urgent] Error while creating recurring %s for %s" % (doctype, doc), + message = frappe.get_template("templates/emails/recurring_sales_invoice_failed.html").render({ + "type": doctype, + "name": doc, + "customer": customer + })) \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index fb7e360940..e418ee91db 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -1,5 +1,6 @@ { - "allow_import": 1, + "allow_attach": 1, + "allow_import": 1, "autoname": "naming_series:", "creation": "2013-06-18 12:39:59", "docstatus": 0, @@ -169,6 +170,24 @@ "search_index": 1, "width": "160px" }, + { + "allow_on_submit": 1, + "description": "Start date of current order's period", + "fieldname": "period_from", + "fieldtype": "Date", + "label": "Order Period From", + "no_copy": 1, + "permlevel": 0 + }, + { + "allow_on_submit": 1, + "description": "End date of current order's period", + "fieldname": "period_to", + "fieldtype": "Date", + "label": "Order Period To", + "no_copy": 1, + "permlevel": 0 + }, { "description": "Customer's Purchase Order Number", "fieldname": "po_no", @@ -888,13 +907,121 @@ "options": "Sales Team", "permlevel": 0, "print_hide": 1 + }, + { + "fieldname": "recurring_order", + "fieldtype": "Section Break", + "label": "Recurring Order", + "options": "icon-time", + "permlevel": 0 + }, + { + "fieldname": "column_break82", + "fieldtype": "Column Break", + "label": "Column Break", + "permlevel": 0 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.docstatus<2", + "description": "Check if recurring order, uncheck to stop recurring or put proper End Date", + "fieldname": "convert_into_recurring", + "fieldtype": "Check", + "label": "Convert into Recurring Order", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.convert_into_recurring==1", + "description": "Select the period when the invoice will be generated automatically", + "fieldname": "recurring_type", + "fieldtype": "Select", + "label": "Recurring Type", + "no_copy": 1, + "options": "\nMonthly\nQuarterly\nHalf-yearly\nYearly", + "permlevel": 0, + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.convert_into_recurring==1", + "description": "The day of the month on which auto order will be generated e.g. 05, 28 etc ", + "fieldname": "repeat_on_day_of_month", + "fieldtype": "Int", + "label": "Repeat on Day of Month", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1 + }, + { + "depends_on": "eval:doc.convert_into_recurring==1", + "description": "The date on which next invoice will be generated. It is generated on submit.", + "fieldname": "next_date", + "fieldtype": "Date", + "label": "Next Date", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.convert_into_recurring==1", + "description": "The date on which recurring order will be stop", + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1 + }, + { + "fieldname": "column_break83", + "fieldtype": "Column Break", + "label": "Column Break", + "permlevel": 0, + "print_hide": 1 + }, + { + "depends_on": "eval:doc.convert_into_recurring==1", + "fieldname": "recurring_id", + "fieldtype": "Data", + "label": "Recurring Id", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.convert_into_recurring==1", + "description": "Enter email id separated by commas, order will be mailed automatically on particular date", + "fieldname": "notification_email_address", + "fieldtype": "Small Text", + "ignore_user_permissions": 0, + "label": "Notification Email Address", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1 + }, + { + "fieldname": "against_income_account", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Against Income Account", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1, + "report_hide": 1 } ], "icon": "icon-file-text", "idx": 1, "is_submittable": 1, "issingle": 0, - "modified": "2014-08-11 07:28:11.362232", + "modified": "2014-08-25 17:41:39.456399", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 37b26fdb48..37aca0a8ba 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -120,6 +120,8 @@ class SalesOrder(SellingController): if not self.billing_status: self.billing_status = 'Not Billed' if not self.delivery_status: self.delivery_status = 'Not Delivered' + self.validate_recurring_document() + def validate_warehouse(self): from erpnext.stock.utils import validate_warehouse_company @@ -161,6 +163,8 @@ class SalesOrder(SellingController): self.update_prevdoc_status('submit') frappe.db.set(self, 'status', 'Submitted') + + self.convert_to_recurring("SO/REC/.#####", self.transaction_date) def on_cancel(self): # Cannot cancel stopped SO @@ -249,6 +253,10 @@ class SalesOrder(SellingController): def get_portal_page(self): return "order" if self.docstatus==1 else None + def on_update_after_submit(self): + self.validate_recurring_document() + self.convert_to_recurring("SO/REC/.#####", self.transaction_date) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): diff --git a/erpnext/templates/emails/recurring_invoice_failed.html b/erpnext/templates/emails/recurring_invoice_failed.html index 39690d8a85..a216e286a5 100644 --- a/erpnext/templates/emails/recurring_invoice_failed.html +++ b/erpnext/templates/emails/recurring_invoice_failed.html @@ -1,12 +1,12 @@ -

Recurring Invoice Failed

+

Recurring {{ type }} Failed

-

An error occured while creating recurring invoice {{ name }} for {{ customer }}.

-

This could be because of some invalid email ids in the invoice.

+

An error occured while creating recurring {{ type }} {{ name }} for {{ customer }}.

+

This could be because of some invalid email ids in the {{ type }}.

To stop sending repetitive error notifications from the system, we have unchecked -"Convert into Recurring" field in the invoice {{ name }}.

-

Please correct the invoice and make the invoice recurring again.

+"Convert into Recurring" field in the {{ type }} {{ name }}.

+

Please correct the {{ type }} and make the {{ type }} recurring again.


-

It is necessary to take this action today itself for the above mentioned recurring invoice +

It is necessary to take this action today itself for the above mentioned recurring {{ type }} to be generated. If delayed, you will have to manually change the "Repeat on Day of Month" field -of this invoice for generating the recurring invoice.

+of this {{ type }} for generating the recurring {{ type }}.

[This email is autogenerated]