Commonify Recurring Sales Order/Invoice
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 76092ed..5228b0e 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -228,7 +228,7 @@
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 @@
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 ff256dc..7cab4c2 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 481ae09..69a7def 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -75,7 +75,7 @@
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 @@
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 @@
})
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 @@
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 ab361d8..44bd451 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 @@
"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 @@
# 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 @@
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 @@
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 @@
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 @@
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 @@
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 59a49af..9aa93ac 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -444,6 +444,57 @@
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 0000000..ad32371
--- /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 fb7e360..e418ee9 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,
@@ -170,6 +171,24 @@
"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",
"fieldtype": "Data",
@@ -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 37b26fd..37aca0a 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -120,6 +120,8 @@
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 @@
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 @@
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 39690d8..a216e28 100644
--- a/erpnext/templates/emails/recurring_invoice_failed.html
+++ b/erpnext/templates/emails/recurring_invoice_failed.html
@@ -1,12 +1,12 @@
-<h2>Recurring Invoice Failed</h2>
+<h2>Recurring {{ type }} Failed</h2>
-<p>An error occured while creating recurring invoice <b>{{ name }}</b> for <b>{{ customer }}</b>.</p>
-<p>This could be because of some invalid email ids in the invoice.</p>
+<p>An error occured while creating recurring {{ type }} <b>{{ name }}</b> for <b>{{ customer }}</b>.</p>
+<p>This could be because of some invalid email ids in the {{ type }}.</p>
<p>To stop sending repetitive error notifications from the system, we have unchecked
-"Convert into Recurring" field in the invoice {{ name }}.</p>
-<p><b>Please correct the invoice and make the invoice recurring again.</b></p>
+"Convert into Recurring" field in the {{ type }} {{ name }}.</p>
+<p><b>Please correct the {{ type }} and make the {{ type }} recurring again.</b></p>
<hr>
-<p><b>It is necessary to take this action today itself for the above mentioned recurring invoice
+<p><b>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.</b></p>
+of this {{ type }} for generating the recurring {{ type }}.</b></p>
<p>[This email is autogenerated]</p>