Commonify Recurring Sales Order/Invoice

This commit is contained in:
ankitjavalkarwork
2014-08-25 18:00:12 +05:30
parent 21b8d30c80
commit e8331d40f3
9 changed files with 479 additions and 171 deletions

View File

@@ -228,7 +228,7 @@ cur_frm.cscript.hide_fields = function(doc) {
par_flds = ['project_name', 'due_date', 'is_opening', 'source', 'total_advance', 'gross_profit', par_flds = ['project_name', 'due_date', 'is_opening', 'source', 'total_advance', 'gross_profit',
'gross_profit_percent', 'get_advances_received', 'gross_profit_percent', 'get_advances_received',
'advance_adjustment_details', 'sales_partner', 'commission_rate', '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'] 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"]); refresh_many(["notification_email_address", "repeat_on_day_of_month"]);
} }
cur_frm.cscript.invoice_period_from_date = function(doc, dt, dn) { cur_frm.cscript.period_from = function(doc, dt, dn) {
// set invoice_period_to_date // set period_to
if(doc.invoice_period_from_date) { if(doc.period_from) {
var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6,
'Yearly': 12}; 'Yearly': 12};
var months = recurring_type_map[doc.recurring_type]; var months = recurring_type_map[doc.recurring_type];
if(months) { 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); months);
doc.invoice_period_to_date = frappe.datetime.add_days(to_date, -1); doc.period_to = frappe.datetime.add_days(to_date, -1);
refresh_field('invoice_period_to_date'); refresh_field('period_to');
} }
} }
} }

View File

@@ -1,5 +1,6 @@
{ {
"allow_import": 1, "allow_attach": 1,
"allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-05-24 19:29:05", "creation": "2013-05-24 19:29:05",
"default_print_format": "Standard", "default_print_format": "Standard",
@@ -172,7 +173,7 @@
"allow_on_submit": 1, "allow_on_submit": 1,
"depends_on": "", "depends_on": "",
"description": "Start date of current invoice's period", "description": "Start date of current invoice's period",
"fieldname": "invoice_period_from_date", "fieldname": "period_from",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Invoice Period From", "label": "Invoice Period From",
"no_copy": 1, "no_copy": 1,
@@ -184,7 +185,7 @@
"allow_on_submit": 1, "allow_on_submit": 1,
"depends_on": "", "depends_on": "",
"description": "End date of current invoice's period", "description": "End date of current invoice's period",
"fieldname": "invoice_period_to_date", "fieldname": "period_to",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Invoice Period To", "label": "Invoice Period To",
"no_copy": 1, "no_copy": 1,
@@ -1087,7 +1088,7 @@
"allow_on_submit": 1, "allow_on_submit": 1,
"depends_on": "eval:doc.docstatus<2", "depends_on": "eval:doc.docstatus<2",
"description": "Check if recurring invoice, uncheck to stop recurring or put proper End Date", "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", "fieldtype": "Check",
"label": "Convert into Recurring Invoice", "label": "Convert into Recurring Invoice",
"no_copy": 1, "no_copy": 1,
@@ -1097,7 +1098,7 @@
}, },
{ {
"allow_on_submit": 1, "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", "description": "Select the period when the invoice will be generated automatically",
"fieldname": "recurring_type", "fieldname": "recurring_type",
"fieldtype": "Select", "fieldtype": "Select",
@@ -1110,7 +1111,7 @@
}, },
{ {
"allow_on_submit": 1, "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 ", "description": "The day of the month on which auto invoice will be generated e.g. 05, 28 etc ",
"fieldname": "repeat_on_day_of_month", "fieldname": "repeat_on_day_of_month",
"fieldtype": "Int", "fieldtype": "Int",
@@ -1121,7 +1122,7 @@
"read_only": 0 "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", "description": "The date on which next invoice will be generated. It is generated on submit.\n",
"fieldname": "next_date", "fieldname": "next_date",
"fieldtype": "Date", "fieldtype": "Date",
@@ -1133,7 +1134,7 @@
}, },
{ {
"allow_on_submit": 1, "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", "description": "The date on which recurring invoice will be stop",
"fieldname": "end_date", "fieldname": "end_date",
"fieldtype": "Date", "fieldtype": "Date",
@@ -1153,7 +1154,7 @@
"width": "50%" "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.", "description": "The unique id for tracking all recurring invoices.\u00a0It is generated on submit.",
"fieldname": "recurring_id", "fieldname": "recurring_id",
"fieldtype": "Data", "fieldtype": "Data",
@@ -1165,7 +1166,7 @@
}, },
{ {
"allow_on_submit": 1, "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", "description": "Enter email id separated by commas, invoice will be mailed automatically on particular date",
"fieldname": "notification_email_address", "fieldname": "notification_email_address",
"fieldtype": "Small Text", "fieldtype": "Small Text",
@@ -1192,7 +1193,7 @@
"icon": "icon-file-text", "icon": "icon-file-text",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2014-08-14 02:13:09.673510", "modified": "2014-08-25 17:41:35.367233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -75,7 +75,7 @@ class SalesInvoice(SellingController):
self.set_against_income_account() self.set_against_income_account()
self.validate_c_form() self.validate_c_form()
self.validate_time_logs_are_submitted() self.validate_time_logs_are_submitted()
self.validate_recurring_invoice() self.validate_recurring_document()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", self.validate_multiple_billing("Delivery Note", "dn_detail", "amount",
"delivery_note_details") "delivery_note_details")
@@ -103,7 +103,7 @@ class SalesInvoice(SellingController):
self.update_c_form() self.update_c_form()
self.update_time_log_batch(self.name) self.update_time_log_batch(self.name)
self.convert_to_recurring() self.convert_to_recurring("RECINV.#####", self.transaction_date)
def before_cancel(self): def before_cancel(self):
self.update_time_log_batch(None) self.update_time_log_batch(None)
@@ -144,8 +144,8 @@ class SalesInvoice(SellingController):
}) })
def on_update_after_submit(self): def on_update_after_submit(self):
self.validate_recurring_invoice() self.validate_recurring_document()
self.convert_to_recurring() self.convert_to_recurring("RECINV.#####", self.transaction_date)
def get_portal_page(self): def get_portal_page(self):
return "invoice" if self.docstatus==1 else None 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""", grand_total = %s where invoice_no = %s and parent = %s""",
(self.name, self.amended_from, self.c_form_no)) (self.name, self.amended_from, self.c_form_no))
def validate_recurring_invoice(self): # def validate_recurring_invoice(self):
if self.convert_into_recurring_invoice: # if self.convert_into_recurring_invoice:
self.validate_notification_email_id() # self.validate_notification_email_id()
if not self.recurring_type: # if not self.recurring_type:
msgprint(_("Please select {0}").format(self.meta.get_label("recurring_type")), # msgprint(_("Please select {0}").format(self.meta.get_label("recurring_type")),
raise_exception=1) # raise_exception=1)
elif not (self.invoice_period_from_date and \ # elif not (self.period_from and \
self.invoice_period_to_date): # self.period_to):
throw(_("Invoice Period From and Invoice Period To dates mandatory for recurring invoice")) # throw(_("Invoice Period From and Invoice Period To dates mandatory for recurring invoice"))
def convert_to_recurring(self): # def convert_to_recurring(self):
if self.convert_into_recurring_invoice: # if self.convert_into_recurring_invoice:
if not self.recurring_id: # if not self.recurring_id:
frappe.db.set(self, "recurring_id", # frappe.db.set(self, "recurring_id",
make_autoname("RECINV/.#####")) # make_autoname("RECINV/.#####"))
self.set_next_date() # self.set_next_date()
elif self.recurring_id: # elif self.recurring_id:
frappe.db.sql("""update `tabSales Invoice` # frappe.db.sql("""update `tabSales Invoice`
set convert_into_recurring_invoice = 0 # set convert_into_recurring_invoice = 0
where recurring_id = %s""", (self.recurring_id,)) # where recurring_id = %s""", (self.recurring_id,))
def validate_notification_email_id(self): # def validate_notification_email_id(self):
if self.notification_email_address: # if self.notification_email_address:
email_list = filter(None, [cstr(email).strip() for email in # email_list = filter(None, [cstr(email).strip() for email in
self.notification_email_address.replace("\n", "").split(",")]) # self.notification_email_address.replace("\n", "").split(",")])
from frappe.utils import validate_email_add # from frappe.utils import validate_email_add
for email in email_list: # for email in email_list:
if not validate_email_add(email): # if not validate_email_add(email):
throw(_("{0} is an invalid email address in 'Notification Email Address'").format(email)) # throw(_("{0} is an invalid email address in 'Notification Email Address'").format(email))
else: # else:
throw(_("'Notification Email Addresses' not specified for recurring invoice")) # throw(_("'Notification Email Addresses' not specified for recurring invoice"))
def set_next_date(self): # def set_next_date(self):
""" Set next date on which auto invoice will be created""" # """ Set next date on which auto invoice will be created"""
if not self.repeat_on_day_of_month: # if not self.repeat_on_day_of_month:
msgprint(_("Please enter 'Repeat on Day of Month' field value"), raise_exception=1) # msgprint(_("Please enter 'Repeat on Day of Month' field value"), raise_exception=1)
next_date = get_next_date(self.posting_date, # next_date = get_next_date(self.posting_date,
month_map[self.recurring_type], cint(self.repeat_on_day_of_month)) # 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): # def get_next_date(dt, mcount, day=None):
dt = getdate(dt) # dt = getdate(dt)
from dateutil.relativedelta import relativedelta # from dateutil.relativedelta import relativedelta
dt += relativedelta(months=mcount, day=day) # dt += relativedelta(months=mcount, day=day)
return dt # return dt
def manage_recurring_invoices(next_date=None, commit=True): # def manage_recurring_invoices(next_date=None, commit=True):
""" # """
Create recurring invoices on specific date by copying the original one # Create recurring invoices on specific date by copying the original one
and notify the concerned people # and notify the concerned people
""" # """
next_date = next_date or nowdate() # next_date = next_date or nowdate()
recurring_invoices = frappe.db.sql("""select name, recurring_id # recurring_invoices = frappe.db.sql("""select name, recurring_id
from `tabSales Invoice` where ifnull(convert_into_recurring_invoice, 0)=1 # from `tabSales Invoice` where ifnull(convert_into_recurring_invoice, 0)=1
and docstatus=1 and next_date=%s # and docstatus=1 and next_date=%s
and next_date <= ifnull(end_date, '2199-12-31')""", next_date) # and next_date <= ifnull(end_date, '2199-12-31')""", next_date)
exception_list = [] # exception_list = []
for ref_invoice, recurring_id in recurring_invoices: # for ref_invoice, recurring_id in recurring_invoices:
if not frappe.db.sql("""select name from `tabSales Invoice` # if not frappe.db.sql("""select name from `tabSales Invoice`
where posting_date=%s and recurring_id=%s and docstatus=1""", # where posting_date=%s and recurring_id=%s and docstatus=1""",
(next_date, recurring_id)): # (next_date, recurring_id)):
try: # try:
ref_wrapper = frappe.get_doc('Sales Invoice', ref_invoice) # ref_wrapper = frappe.get_doc('Sales Invoice', ref_invoice)
new_invoice_wrapper = make_new_invoice(ref_wrapper, next_date) # new_invoice_wrapper = make_new_invoice(ref_wrapper, next_date)
send_notification(new_invoice_wrapper) # send_notification(new_invoice_wrapper)
if commit: # if commit:
frappe.db.commit() # frappe.db.commit()
except: # except:
if commit: # if commit:
frappe.db.rollback() # frappe.db.rollback()
frappe.db.begin() # frappe.db.begin()
frappe.db.sql("update `tabSales Invoice` set \ # frappe.db.sql("update `tabSales Invoice` set \
convert_into_recurring_invoice = 0 where name = %s", ref_invoice) # convert_into_recurring_invoice = 0 where name = %s", ref_invoice)
notify_errors(ref_invoice, ref_wrapper.customer, ref_wrapper.owner) # notify_errors(ref_invoice, ref_wrapper.customer, ref_wrapper.owner)
frappe.db.commit() # frappe.db.commit()
exception_list.append(frappe.get_traceback()) # exception_list.append(frappe.get_traceback())
finally: # finally:
if commit: # if commit:
frappe.db.begin() # frappe.db.begin()
if exception_list: # if exception_list:
exception_message = "\n\n".join([cstr(d) for d in exception_list]) # exception_message = "\n\n".join([cstr(d) for d in exception_list])
frappe.throw(exception_message) # frappe.throw(exception_message)
def make_new_invoice(ref_wrapper, posting_date): # def make_new_invoice(ref_wrapper, posting_date):
from erpnext.accounts.utils import get_fiscal_year # from erpnext.accounts.utils import get_fiscal_year
new_invoice = frappe.copy_doc(ref_wrapper) # 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 # # 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 # # and to date is the last day of its own month
if (cstr(get_first_day(ref_wrapper.invoice_period_from_date)) == \ # if (cstr(get_first_day(ref_wrapper.period_from)) == \
cstr(ref_wrapper.invoice_period_from_date)) and \ # cstr(ref_wrapper.period_from)) and \
(cstr(get_last_day(ref_wrapper.invoice_period_to_date)) == \ # (cstr(get_last_day(ref_wrapper.period_to)) == \
cstr(ref_wrapper.invoice_period_to_date)): # cstr(ref_wrapper.period_to)):
invoice_period_to_date = get_last_day(get_next_date(ref_wrapper.invoice_period_to_date, # period_to = get_last_day(get_next_date(ref_wrapper.period_to,
mcount)) # mcount))
else: # else:
invoice_period_to_date = get_next_date(ref_wrapper.invoice_period_to_date, mcount) # period_to = get_next_date(ref_wrapper.period_to, mcount)
new_invoice.update({ # new_invoice.update({
"posting_date": posting_date, # "posting_date": posting_date,
"aging_date": posting_date, # "aging_date": posting_date,
"due_date": add_days(posting_date, cint(date_diff(ref_wrapper.due_date, # "due_date": add_days(posting_date, cint(date_diff(ref_wrapper.due_date,
ref_wrapper.posting_date))), # ref_wrapper.posting_date))),
"invoice_period_from_date": invoice_period_from_date, # "period_from": period_from,
"invoice_period_to_date": invoice_period_to_date, # "period_to": period_to,
"fiscal_year": get_fiscal_year(posting_date)[0], # "fiscal_year": get_fiscal_year(posting_date)[0],
"owner": ref_wrapper.owner, # "owner": ref_wrapper.owner,
}) # })
new_invoice.submit() # new_invoice.submit()
return new_invoice # return new_invoice
def send_notification(new_rv): # def send_notification(new_rv):
"""Notify concerned persons about recurring invoice generation""" # """Notify concerned persons about recurring invoice generation"""
frappe.sendmail(new_rv.notification_email_address, # frappe.sendmail(new_rv.notification_email_address,
subject="New Invoice : " + new_rv.name, # subject="New Invoice : " + new_rv.name,
message = _("Please find attached Sales Invoice #{0}").format(new_rv.name), # message = _("Please find attached Sales Invoice #{0}").format(new_rv.name),
attachments = [{ # attachments = [{
"fname": new_rv.name + ".pdf", # "fname": new_rv.name + ".pdf",
"fcontent": frappe.get_print_format(new_rv.doctype, new_rv.name, as_pdf=True) # "fcontent": frappe.get_print_format(new_rv.doctype, new_rv.name, as_pdf=True)
}]) # }])
def notify_errors(inv, customer, owner): # def notify_errors(inv, customer, owner):
from frappe.utils.user import get_system_managers # from frappe.utils.user import get_system_managers
recipients=get_system_managers(only_name=True) # recipients=get_system_managers(only_name=True)
frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")], # frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")],
subject="[Urgent] Error while creating recurring invoice for %s" % inv, # subject="[Urgent] Error while creating recurring invoice for %s" % inv,
message = frappe.get_template("templates/emails/recurring_invoice_failed.html").render({ # message = frappe.get_template("templates/emails/recurring_invoice_failed.html").render({
"name": inv, # "name": inv,
"customer": customer # "customer": customer
})) # }))
assign_task_to_owner(inv, "Recurring Invoice Failed", recipients) assign_task_to_owner(inv, "Recurring Invoice Failed", recipients)

View File

@@ -677,8 +677,8 @@ class TestSalesInvoice(unittest.TestCase):
"posting_date": today, "posting_date": today,
"due_date": None, "due_date": None,
"fiscal_year": get_fiscal_year(today)[0], "fiscal_year": get_fiscal_year(today)[0],
"invoice_period_from_date": get_first_day(today), "period_from": get_first_day(today),
"invoice_period_to_date": get_last_day(today) "period_to": get_last_day(today)
}) })
# monthly # monthly
@@ -690,8 +690,8 @@ class TestSalesInvoice(unittest.TestCase):
# monthly without a first and last day period # monthly without a first and last day period
si2 = frappe.copy_doc(base_si) si2 = frappe.copy_doc(base_si)
si2.update({ si2.update({
"invoice_period_from_date": today, "period_from": today,
"invoice_period_to_date": add_to_date(today, days=30) "period_to": add_to_date(today, days=30)
}) })
si2.insert() si2.insert()
si2.submit() si2.submit()
@@ -701,8 +701,8 @@ class TestSalesInvoice(unittest.TestCase):
si3 = frappe.copy_doc(base_si) si3 = frappe.copy_doc(base_si)
si3.update({ si3.update({
"recurring_type": "Quarterly", "recurring_type": "Quarterly",
"invoice_period_from_date": get_first_day(today), "period_from": get_first_day(today),
"invoice_period_to_date": get_last_day(add_to_date(today, months=3)) "period_to": get_last_day(add_to_date(today, months=3))
}) })
si3.insert() si3.insert()
si3.submit() si3.submit()
@@ -712,8 +712,8 @@ class TestSalesInvoice(unittest.TestCase):
si4 = frappe.copy_doc(base_si) si4 = frappe.copy_doc(base_si)
si4.update({ si4.update({
"recurring_type": "Quarterly", "recurring_type": "Quarterly",
"invoice_period_from_date": today, "period_from": today,
"invoice_period_to_date": add_to_date(today, months=3) "period_to": add_to_date(today, months=3)
}) })
si4.insert() si4.insert()
si4.submit() si4.submit()
@@ -723,8 +723,8 @@ class TestSalesInvoice(unittest.TestCase):
si5 = frappe.copy_doc(base_si) si5 = frappe.copy_doc(base_si)
si5.update({ si5.update({
"recurring_type": "Yearly", "recurring_type": "Yearly",
"invoice_period_from_date": get_first_day(today), "period_from": get_first_day(today),
"invoice_period_to_date": get_last_day(add_to_date(today, years=1)) "period_to": get_last_day(add_to_date(today, years=1))
}) })
si5.insert() si5.insert()
si5.submit() si5.submit()
@@ -734,8 +734,8 @@ class TestSalesInvoice(unittest.TestCase):
si6 = frappe.copy_doc(base_si) si6 = frappe.copy_doc(base_si)
si6.update({ si6.update({
"recurring_type": "Yearly", "recurring_type": "Yearly",
"invoice_period_from_date": today, "period_from": today,
"invoice_period_to_date": add_to_date(today, years=1) "period_to": add_to_date(today, years=1)
}) })
si6.insert() si6.insert()
si6.submit() si6.submit()
@@ -784,16 +784,16 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEquals(new_si.posting_date, unicode(next_date)) self.assertEquals(new_si.posting_date, unicode(next_date))
self.assertEquals(new_si.invoice_period_from_date, self.assertEquals(new_si.period_from,
unicode(add_months(base_si.invoice_period_from_date, no_of_months))) unicode(add_months(base_si.period_from, no_of_months)))
if first_and_last_day: if first_and_last_day:
self.assertEquals(new_si.invoice_period_to_date, self.assertEquals(new_si.period_to,
unicode(get_last_day(add_months(base_si.invoice_period_to_date, unicode(get_last_day(add_months(base_si.period_to,
no_of_months)))) no_of_months))))
else: else:
self.assertEquals(new_si.invoice_period_to_date, self.assertEquals(new_si.period_to,
unicode(add_months(base_si.invoice_period_to_date, no_of_months))) unicode(add_months(base_si.period_to, no_of_months)))
return new_si return new_si

View File

@@ -444,6 +444,57 @@ class AccountsController(TransactionBase):
if total_outstanding: if total_outstanding:
frappe.get_doc('Account', account).check_credit_limit(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() @frappe.whitelist()
def get_tax_rate(account_head): def get_tax_rate(account_head):

View File

@@ -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
}))

View File

@@ -1,5 +1,6 @@
{ {
"allow_import": 1, "allow_attach": 1,
"allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-06-18 12:39:59", "creation": "2013-06-18 12:39:59",
"docstatus": 0, "docstatus": 0,
@@ -169,6 +170,24 @@
"search_index": 1, "search_index": 1,
"width": "160px" "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", "description": "Customer's Purchase Order Number",
"fieldname": "po_no", "fieldname": "po_no",
@@ -888,13 +907,121 @@
"options": "Sales Team", "options": "Sales Team",
"permlevel": 0, "permlevel": 0,
"print_hide": 1 "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", "icon": "icon-file-text",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "issingle": 0,
"modified": "2014-08-11 07:28:11.362232", "modified": "2014-08-25 17:41:39.456399",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@@ -120,6 +120,8 @@ class SalesOrder(SellingController):
if not self.billing_status: self.billing_status = 'Not Billed' if not self.billing_status: self.billing_status = 'Not Billed'
if not self.delivery_status: self.delivery_status = 'Not Delivered' if not self.delivery_status: self.delivery_status = 'Not Delivered'
self.validate_recurring_document()
def validate_warehouse(self): def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company from erpnext.stock.utils import validate_warehouse_company
@@ -161,6 +163,8 @@ class SalesOrder(SellingController):
self.update_prevdoc_status('submit') self.update_prevdoc_status('submit')
frappe.db.set(self, 'status', 'Submitted') frappe.db.set(self, 'status', 'Submitted')
self.convert_to_recurring("SO/REC/.#####", self.transaction_date)
def on_cancel(self): def on_cancel(self):
# Cannot cancel stopped SO # Cannot cancel stopped SO
@@ -249,6 +253,10 @@ class SalesOrder(SellingController):
def get_portal_page(self): def get_portal_page(self):
return "order" if self.docstatus==1 else None 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() @frappe.whitelist()
def make_material_request(source_name, target_doc=None): def make_material_request(source_name, target_doc=None):

View File

@@ -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>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 invoice.</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 <p>To stop sending repetitive error notifications from the system, we have unchecked
"Convert into Recurring" field in the invoice {{ name }}.</p> "Convert into Recurring" field in the {{ type }} {{ name }}.</p>
<p><b>Please correct the invoice and make the invoice recurring again.</b></p> <p><b>Please correct the {{ type }} and make the {{ type }} recurring again.</b></p>
<hr> <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 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> <p>[This email is autogenerated]</p>