diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 76092ed30d..73832cec65 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', 'from_date', 'to_date']; item_flds_normal = ['sales_order', 'delivery_note'] @@ -399,9 +399,9 @@ cur_frm.cscript.on_submit = function(doc, cdt, cdn) { }) } -cur_frm.cscript.convert_into_recurring_invoice = function(doc, dt, dn) { +cur_frm.cscript.is_recurring = function(doc, dt, dn) { // set default values for recurring invoices - if(doc.convert_into_recurring_invoice) { + if(doc.is_recurring) { var owner_email = doc.owner=="Administrator" ? frappe.user_info("Administrator").email : doc.owner; @@ -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.from_date = function(doc, dt, dn) { + // set to_date + if(doc.from_date) { 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.from_date, months); - doc.invoice_period_to_date = frappe.datetime.add_days(to_date, -1); - refresh_field('invoice_period_to_date'); + doc.to_date = frappe.datetime.add_days(to_date, -1); + refresh_field('to_date'); } } } diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index ff256dc777..c26583b737 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,9 +173,9 @@ "allow_on_submit": 1, "depends_on": "", "description": "Start date of current invoice's period", - "fieldname": "invoice_period_from_date", + "fieldname": "from_date", "fieldtype": "Date", - "label": "Invoice Period From", + "label": "From", "no_copy": 1, "permlevel": 0, "print_hide": 0, @@ -184,9 +185,9 @@ "allow_on_submit": 1, "depends_on": "", "description": "End date of current invoice's period", - "fieldname": "invoice_period_to_date", + "fieldname": "to_date", "fieldtype": "Date", - "label": "Invoice Period To", + "label": "To", "no_copy": 1, "permlevel": 0, "print_hide": 0, @@ -1087,9 +1088,9 @@ "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": "is_recurring", "fieldtype": "Check", - "label": "Convert into Recurring Invoice", + "label": "Is Recurring", "no_copy": 1, "permlevel": 0, "print_hide": 1, @@ -1097,7 +1098,7 @@ }, { "allow_on_submit": 1, - "depends_on": "eval:doc.convert_into_recurring_invoice==1", + "depends_on": "eval:doc.is_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.is_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.is_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.is_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.is_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.is_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-28 11:21:00.726344", "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..a20d906b8c 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -14,7 +14,7 @@ from erpnext.accounts.party import get_party_account, get_due_date from erpnext.controllers.stock_controller import update_gl_entries_after from frappe.model.mapper import get_mapped_doc -month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} +from erpnext.controllers.recurring_document import * from erpnext.controllers.selling_controller import SellingController @@ -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() + validate_recurring_document(self) 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() + convert_to_recurring(self, "RECINV.#####", self.posting_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() + validate_recurring_document(self) + convert_to_recurring(self, "RECINV.#####", self.posting_date) def get_portal_page(self): return "invoice" if self.docstatus==1 else None @@ -592,172 +592,6 @@ 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() - - 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")) - - 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() - - 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(",")]) - - 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")) - - 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)) - - frappe.db.set(self, 'next_date', next_date) - -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 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() - - 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() - - 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) - - mcount = month_map[ref_wrapper.recurring_type] - - invoice_period_from_date = get_next_date(ref_wrapper.invoice_period_from_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.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) - - 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.submit() - - 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 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 - })) - - assign_task_to_owner(inv, "Recurring Invoice Failed", recipients) - -def assign_task_to_owner(inv, msg, users): - for d in users: - from frappe.widgets.form import assign_to - args = { - 'assign_to' : d, - 'doctype' : 'Sales Invoice', - 'name' : inv, - 'description' : msg, - 'priority' : 'High' - } - assign_to.add(args) - @frappe.whitelist() def get_bank_cash_account(mode_of_payment): val = frappe.db.get_value("Mode of Payment", mode_of_payment, "default_account") diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ab361d83ad..8231650167 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -665,143 +665,9 @@ class TestSalesInvoice(unittest.TestCase): where against_invoice=%s""", si.name)) def test_recurring_invoice(self): - from frappe.utils import get_first_day, get_last_day, add_to_date, nowdate, getdate - from erpnext.accounts.utils import get_fiscal_year - today = nowdate() - base_si = frappe.copy_doc(test_records[0]) - base_si.update({ - "convert_into_recurring_invoice": 1, - "recurring_type": "Monthly", - "notification_email_address": "test@example.com, test1@example.com, test2@example.com", - "repeat_on_day_of_month": getdate(today).day, - "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) - }) + from erpnext.controllers.tests.test_recurring_document import test_recurring_document - # monthly - si1 = frappe.copy_doc(base_si) - si1.insert() - si1.submit() - self._test_recurring_invoice(si1, True) - - # 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) - }) - si2.insert() - si2.submit() - self._test_recurring_invoice(si2, False) - - # quarterly - 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)) - }) - si3.insert() - si3.submit() - self._test_recurring_invoice(si3, True) - - # quarterly without a first and last day period - 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) - }) - si4.insert() - si4.submit() - self._test_recurring_invoice(si4, False) - - # yearly - 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)) - }) - si5.insert() - si5.submit() - self._test_recurring_invoice(si5, True) - - # yearly without a first and last day period - 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) - }) - si6.insert() - si6.submit() - self._test_recurring_invoice(si6, False) - - # change posting date but keep recuring day to be today - si7 = frappe.copy_doc(base_si) - si7.update({ - "posting_date": add_to_date(today, days=-1) - }) - si7.insert() - si7.submit() - - # setting so that _test function works - si7.posting_date = today - self._test_recurring_invoice(si7, True) - - def _test_recurring_invoice(self, base_si, first_and_last_day): - from frappe.utils import add_months, get_last_day - from erpnext.accounts.doctype.sales_invoice.sales_invoice \ - import manage_recurring_invoices, get_next_date - - no_of_months = ({"Monthly": 1, "Quarterly": 3, "Yearly": 12})[base_si.recurring_type] - - def _test(i): - self.assertEquals(i+1, frappe.db.sql("""select count(*) from `tabSales Invoice` - where recurring_id=%s and docstatus=1""", base_si.recurring_id)[0][0]) - - next_date = get_next_date(base_si.posting_date, no_of_months, - base_si.repeat_on_day_of_month) - - manage_recurring_invoices(next_date=next_date, commit=False) - - recurred_invoices = frappe.db.sql("""select name from `tabSales Invoice` - where recurring_id=%s and docstatus=1 order by name desc""", - base_si.recurring_id) - - self.assertEquals(i+2, len(recurred_invoices)) - - new_si = frappe.get_doc("Sales Invoice", recurred_invoices[0][0]) - - for fieldname in ["convert_into_recurring_invoice", "recurring_type", - "repeat_on_day_of_month", "notification_email_address"]: - self.assertEquals(base_si.get(fieldname), - new_si.get(fieldname)) - - 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))) - - 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, - no_of_months)))) - else: - self.assertEquals(new_si.invoice_period_to_date, - unicode(add_months(base_si.invoice_period_to_date, no_of_months))) - - - return new_si - - # if yearly, test 1 repetition, else test 5 repetitions - count = 1 if (no_of_months == 12) else 5 - for i in xrange(count): - base_si = _test(i) + test_recurring_document(self, test_records) def clear_stock_account_balance(self): frappe.db.sql("delete from `tabStock Ledger Entry`") diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 59a49afb22..4af9f5ed8a 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -4,7 +4,9 @@ from __future__ import unicode_literals import frappe from frappe import _, throw -from frappe.utils import flt, cint, today +from frappe.utils import add_days, cint, cstr, today, date_diff, flt, getdate, nowdate, \ + get_first_day, get_last_day +from frappe.model.naming import make_autoname from erpnext.setup.utils import get_company_currency, get_exchange_rate from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year from erpnext.utilities.transaction_base import TransactionBase @@ -444,7 +446,6 @@ class AccountsController(TransactionBase): if total_outstanding: frappe.get_doc('Account', account).check_credit_limit(total_outstanding) - @frappe.whitelist() def get_tax_rate(account_head): return frappe.db.get_value("Account", account_head, "tax_rate") diff --git a/erpnext/controllers/recurring_document.py b/erpnext/controllers/recurring_document.py new file mode 100644 index 0000000000..729c6f7f9c --- /dev/null +++ b/erpnext/controllers/recurring_document.py @@ -0,0 +1,199 @@ +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 + +month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} + +def create_recurring_documents(): + manage_recurring_documents("Sales Order") + manage_recurring_documents("Sales Invoice") + +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() + + if doctype == "Sales Order": + date_field = "transaction_date" + elif doctype == "Sales Invoice": + date_field = "posting_date" + + recurring_documents = frappe.db.sql("""select name, recurring_id + from `tab{}` where ifnull(is_recurring, 0)=1 + and docstatus=1 and next_date='{}' + and next_date <= ifnull(end_date, '2199-12-31')""".format(doctype, next_date)) + + exception_list = [] + for ref_document, recurring_id in recurring_documents: + if not frappe.db.sql("""select name from `tab%s` + where %s=%s and recurring_id=%s and docstatus=1""" + % (doctype, date_field, '%s', '%s'), (next_date, recurring_id)): + try: + ref_wrapper = frappe.get_doc(doctype, ref_document) + new_document_wrapper = make_new_document(ref_wrapper, date_field, 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 is_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, date_field, posting_date): + from erpnext.accounts.utils import get_fiscal_year + new_document = frappe.copy_doc(ref_wrapper) + mcount = month_map[ref_wrapper.recurring_type] + + from_date = get_next_date(ref_wrapper.from_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.from_date)) == \ + cstr(ref_wrapper.from_date)) and \ + (cstr(get_last_day(ref_wrapper.to_date)) == \ + cstr(ref_wrapper.to_date)): + to_date = get_last_day(get_next_date(ref_wrapper.to_date, + mcount)) + else: + to_date = get_next_date(ref_wrapper.to_date, mcount) + + new_document.update({ + date_field: posting_date, + "from_date": from_date, + "to_date": to_date, + "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_document_failed.html").render({ + "type": doctype, + "name": doc, + "customer": customer + })) + + assign_task_to_owner(doc, doctype, "Recurring Invoice Failed", recipients) + +def assign_task_to_owner(doc, doctype, msg, users): + for d in users: + from frappe.widgets.form import assign_to + args = { + 'assign_to' : d, + 'doctype' : doctype, + 'name' : doc, + 'description' : msg, + 'priority' : 'High' + } + assign_to.add(args) + +def validate_recurring_document(doc): + if doc.is_recurring: + validate_notification_email_id(doc) + + if not doc.recurring_type: + msgprint(_("Please select {0}").format(doc.meta.get_label("recurring_type")), + raise_exception=1) + + elif not (doc.from_date and doc.to_date): + throw(_("Period From and Period To dates mandatory for recurring %s") % doc.doctype) + +def convert_to_recurring(doc, autoname, posting_date): + if doc.is_recurring: + if not doc.recurring_id: + frappe.db.set(doc, "recurring_id", + make_autoname(autoname)) + + set_next_date(doc, posting_date) + + elif doc.recurring_id: + frappe.db.sql("""update `tab%s` + set is_recurring = 0 + where recurring_id = %s""" % (doc.doctype, '%s'), (doc.recurring_id)) + +def validate_notification_email_id(doc): + if doc.notification_email_address: + email_list = filter(None, [cstr(email).strip() for email in + doc.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") \ + % doc.doctype) + +def set_next_date(doc, posting_date): + """ Set next date on which recurring document will be created""" + + if not doc.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[doc.recurring_type], + cint(doc.repeat_on_day_of_month)) + + frappe.db.set(doc, 'next_date', next_date) + + msgprint(_("Next Recurring {0} will be created on {1}").format(doc.doctype, next_date)) diff --git a/erpnext/controllers/tests/__init__.py b/erpnext/controllers/tests/__init__.py new file mode 100644 index 0000000000..60bec4fbec --- /dev/null +++ b/erpnext/controllers/tests/__init__.py @@ -0,0 +1 @@ +from erpnext.__version__ import __version__ diff --git a/erpnext/controllers/tests/test_recurring_document.py b/erpnext/controllers/tests/test_recurring_document.py new file mode 100644 index 0000000000..0e7cb1bc5e --- /dev/null +++ b/erpnext/controllers/tests/test_recurring_document.py @@ -0,0 +1,166 @@ +# Copyright (c) 2013, Web Notes Technologies Pvt. Ltd. and Contributors +# License: GNU General Public License v3. See license.txt + +import frappe +import unittest, json, copy +from frappe.utils import flt +import frappe.permissions +from erpnext.accounts.utils import get_stock_and_account_difference +from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory +from erpnext.projects.doctype.time_log_batch.test_time_log_batch import * + +def test_recurring_document(obj, test_records): + from frappe.utils import get_first_day, get_last_day, add_to_date, nowdate, getdate, add_days + from erpnext.accounts.utils import get_fiscal_year + frappe.db.set_value("Print Settings", "Print Settings", "send_print_as_pdf", 1) + today = nowdate() + base_doc = frappe.copy_doc(test_records[0]) + + base_doc.update({ + "is_recurring": 1, + "recurring_type": "Monthly", + "notification_email_address": "test@example.com, test1@example.com, test2@example.com", + "repeat_on_day_of_month": getdate(today).day, + "due_date": None, + "fiscal_year": get_fiscal_year(today)[0], + "from_date": get_first_day(today), + "to_date": get_last_day(today) + }) + + if base_doc.doctype == "Sales Order": + base_doc.update({ + "transaction_date": today, + "delivery_date": add_days(today, 15) + }) + elif base_doc.doctype == "Sales Invoice": + base_doc.update({ + "posting_date": today + }) + + if base_doc.doctype == "Sales Order": + date_field = "transaction_date" + elif base_doc.doctype == "Sales Invoice": + date_field = "posting_date" + + # monthly + doc1 = frappe.copy_doc(base_doc) + doc1.insert() + doc1.submit() + _test_recurring_document(obj, doc1, date_field, True) + + # monthly without a first and last day period + doc2 = frappe.copy_doc(base_doc) + doc2.update({ + "from_date": today, + "to_date": add_to_date(today, days=30) + }) + doc2.insert() + doc2.submit() + _test_recurring_document(obj, doc2, date_field, False) + + # quarterly + doc3 = frappe.copy_doc(base_doc) + doc3.update({ + "recurring_type": "Quarterly", + "from_date": get_first_day(today), + "to_date": get_last_day(add_to_date(today, months=3)) + }) + doc3.insert() + doc3.submit() + _test_recurring_document(obj, doc3, date_field, True) + + # quarterly without a first and last day period + doc4 = frappe.copy_doc(base_doc) + doc4.update({ + "recurring_type": "Quarterly", + "from_date": today, + "to_date": add_to_date(today, months=3) + }) + doc4.insert() + doc4.submit() + _test_recurring_document(obj, doc4, date_field, False) + + # yearly + doc5 = frappe.copy_doc(base_doc) + doc5.update({ + "recurring_type": "Yearly", + "from_date": get_first_day(today), + "to_date": get_last_day(add_to_date(today, years=1)) + }) + doc5.insert() + doc5.submit() + _test_recurring_document(obj, doc5, date_field, True) + + # yearly without a first and last day period + doc6 = frappe.copy_doc(base_doc) + doc6.update({ + "recurring_type": "Yearly", + "from_date": today, + "to_date": add_to_date(today, years=1) + }) + doc6.insert() + doc6.submit() + _test_recurring_document(obj, doc6, date_field, False) + + # change date field but keep recurring day to be today + doc7 = frappe.copy_doc(base_doc) + doc7.update({ + date_field: today, + }) + doc7.insert() + doc7.submit() + + # setting so that _test function works + # doc7.set(date_field, today) + _test_recurring_document(obj, doc7, date_field, True) + +def _test_recurring_document(obj, base_doc, date_field, first_and_last_day): + from frappe.utils import add_months, get_last_day + from erpnext.controllers.recurring_document import manage_recurring_documents, \ + get_next_date + + no_of_months = ({"Monthly": 1, "Quarterly": 3, "Yearly": 12})[base_doc.recurring_type] + + def _test(i): + obj.assertEquals(i+1, frappe.db.sql("""select count(*) from `tab%s` + where recurring_id=%s and docstatus=1""" % (base_doc.doctype, '%s'), + (base_doc.recurring_id))[0][0]) + + next_date = get_next_date(base_doc.get(date_field), no_of_months, + base_doc.repeat_on_day_of_month) + + manage_recurring_documents(base_doc.doctype, next_date=next_date, commit=False) + + recurred_documents = frappe.db.sql("""select name from `tab%s` + where recurring_id=%s and docstatus=1 order by name desc""" + % (base_doc.doctype, '%s'), (base_doc.recurring_id)) + + obj.assertEquals(i+2, len(recurred_documents)) + + new_doc = frappe.get_doc(base_doc.doctype, recurred_documents[0][0]) + + for fieldname in ["is_recurring", "recurring_type", + "repeat_on_day_of_month", "notification_email_address"]: + obj.assertEquals(base_doc.get(fieldname), + new_doc.get(fieldname)) + + obj.assertEquals(new_doc.get(date_field), unicode(next_date)) + + obj.assertEquals(new_doc.from_date, + unicode(add_months(base_doc.from_date, no_of_months))) + + if first_and_last_day: + obj.assertEquals(new_doc.to_date, + unicode(get_last_day(add_months(base_doc.to_date, + no_of_months)))) + else: + obj.assertEquals(new_doc.to_date, + unicode(add_months(base_doc.to_date, no_of_months))) + + + return new_doc + + # if yearly, test 1 repetition, else test 5 repetitions + count = 1 if (no_of_months == 12) else 5 + for i in xrange(count): + base_doc = _test(i) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index df15916f7a..b9e8451f18 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -64,7 +64,7 @@ scheduler_events = { "erpnext.selling.doctype.lead.get_leads.get_leads" ], "daily": [ - "erpnext.accounts.doctype.sales_invoice.sales_invoice.manage_recurring_invoices", + "erpnext.controllers.recurring_document.create_recurring_documents" "erpnext.stock.utils.reorder_item", "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.support.doctype.support_ticket.support_ticket.auto_close_tickets" diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d744fee784..cde43f347e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -78,4 +78,8 @@ erpnext.patches.v4_2.update_project_milestones erpnext.patches.v4_2.add_currency_turkish_lira #2014-08-08 execute:frappe.delete_doc("DocType", "Landed Cost Wizard") erpnext.patches.v4_2.default_website_style +<<<<<<< HEAD erpnext.patches.v4_2.set_company_country +======= +erpnext.patches.v4_2.update_sales_order_invoice_field_name +>>>>>>> Add patch for field name change in SI, rename email template diff --git a/erpnext/patches/v4_2/update_sales_order_invoice_field_name.py b/erpnext/patches/v4_2/update_sales_order_invoice_field_name.py new file mode 100644 index 0000000000..a8303a0aae --- /dev/null +++ b/erpnext/patches/v4_2/update_sales_order_invoice_field_name.py @@ -0,0 +1,6 @@ +import frappe + +def execute(): + frappe.reload_doc('accounts', 'doctype', 'sales_invoice') + frappe.db.sql("""update `tabSales Invoice` set from_date = invoice_period_from_date, + to_date = invoice_period_to_date, is_recurring = convert_into_recurring_invoice""") diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 628e43e1df..4797230ad0 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -195,6 +195,37 @@ cur_frm.cscript.on_submit = function(doc, cdt, cdn) { } }; +cur_frm.cscript.is_recurring = function(doc, dt, dn) { + // set default values for recurring orders + if(doc.is_recurring) { + var owner_email = doc.owner=="Administrator" + ? frappe.user_info("Administrator").email + : doc.owner; + + doc.notification_email_address = $.map([cstr(owner_email), + cstr(doc.contact_email)], function(v) { return v || null; }).join(", "); + doc.repeat_on_day_of_month = frappe.datetime.str_to_obj(doc.posting_date).getDate(); + } + + refresh_many(["notification_email_address", "repeat_on_day_of_month"]); +} + +cur_frm.cscript.from_date = function(doc, dt, dn) { + // set to_date + if(doc.from_date) { + 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.from_date, + months); + doc.to_date = frappe.datetime.add_days(to_date, -1); + refresh_field('to_date'); + } + } +} + cur_frm.cscript.send_sms = function() { frappe.require("assets/erpnext/js/sms_manager.js"); var sms_man = new SMSManager(cur_frm.doc); diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index fb7e360940..a4b00ff8b6 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": "from_date", + "fieldtype": "Date", + "label": "From", + "no_copy": 1, + "permlevel": 0 + }, + { + "allow_on_submit": 1, + "description": "End date of current order's period", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "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": "is_recurring", + "fieldtype": "Check", + "label": "Is Recurring", + "no_copy": 1, + "permlevel": 0, + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.is_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.is_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.is_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.is_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.is_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.is_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-28 11:22:10.959416", "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..ff14f9d0c1 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -10,6 +10,8 @@ from frappe.utils import cstr, flt, getdate, comma_and from frappe import _ from frappe.model.mapper import get_mapped_doc +from erpnext.controllers.recurring_document import convert_to_recurring, validate_recurring_document + from erpnext.controllers.selling_controller import SellingController form_grid_templates = { @@ -120,6 +122,8 @@ class SalesOrder(SellingController): if not self.billing_status: self.billing_status = 'Not Billed' if not self.delivery_status: self.delivery_status = 'Not Delivered' + validate_recurring_document(self) + def validate_warehouse(self): from erpnext.stock.utils import validate_warehouse_company @@ -161,6 +165,8 @@ class SalesOrder(SellingController): self.update_prevdoc_status('submit') frappe.db.set(self, 'status', 'Submitted') + + convert_to_recurring(self, "SO/REC/.#####", self.transaction_date) def on_cancel(self): # Cannot cancel stopped SO @@ -249,6 +255,10 @@ class SalesOrder(SellingController): def get_portal_page(self): return "order" if self.docstatus==1 else None + def on_update_after_submit(self): + validate_recurring_document(self) + convert_to_recurring(self, "SO/REC/.#####", self.transaction_date) + @frappe.whitelist() def make_material_request(source_name, target_doc=None): diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 128c5774d9..c55b7b383f 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -331,6 +331,11 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.CancelledLinkError, delivery_note.submit) + def test_recurring_order(self): + from erpnext.controllers.tests.test_recurring_document import test_recurring_document + + test_recurring_document(self, test_records) + test_dependencies = ["Sales BOM", "Currency Exchange"] test_records = frappe.get_test_records('Sales Order') diff --git a/erpnext/templates/emails/recurring_document_failed.html b/erpnext/templates/emails/recurring_document_failed.html new file mode 100644 index 0000000000..a216e286a5 --- /dev/null +++ b/erpnext/templates/emails/recurring_document_failed.html @@ -0,0 +1,12 @@ +

Recurring {{ type }} Failed

+ +

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 {{ 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 {{ type }} +to be generated. If delayed, you will have to manually change the "Repeat on Day of Month" field +of this {{ type }} for generating the recurring {{ type }}.

+

[This email is autogenerated]

diff --git a/erpnext/templates/emails/recurring_invoice_failed.html b/erpnext/templates/emails/recurring_invoice_failed.html deleted file mode 100644 index 39690d8a85..0000000000 --- a/erpnext/templates/emails/recurring_invoice_failed.html +++ /dev/null @@ -1,12 +0,0 @@ -

Recurring Invoice Failed

- -

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

-

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

-

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.

-
-

It is necessary to take this action today itself for the above mentioned recurring invoice -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.

-

[This email is autogenerated]