diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 44bd451ed8d..c84d1721297 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -665,143 +665,148 @@ 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], - "period_from": get_first_day(today), - "period_to": 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) + test_recurring_document(self, test_records) - # monthly without a first and last day period - si2 = frappe.copy_doc(base_si) - si2.update({ - "period_from": today, - "period_to": add_to_date(today, days=30) - }) - si2.insert() - si2.submit() - self._test_recurring_invoice(si2, False) + # 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], + # "period_from": get_first_day(today), + # "period_to": get_last_day(today) + # }) - # quarterly - si3 = frappe.copy_doc(base_si) - si3.update({ - "recurring_type": "Quarterly", - "period_from": get_first_day(today), - "period_to": get_last_day(add_to_date(today, months=3)) - }) - si3.insert() - si3.submit() - self._test_recurring_invoice(si3, True) + # # monthly + # si1 = frappe.copy_doc(base_si) + # si1.insert() + # si1.submit() + # self._test_recurring_invoice(si1, True) - # quarterly without a first and last day period - si4 = frappe.copy_doc(base_si) - si4.update({ - "recurring_type": "Quarterly", - "period_from": today, - "period_to": add_to_date(today, months=3) - }) - si4.insert() - si4.submit() - self._test_recurring_invoice(si4, False) + # # monthly without a first and last day period + # si2 = frappe.copy_doc(base_si) + # si2.update({ + # "period_from": today, + # "period_to": add_to_date(today, days=30) + # }) + # si2.insert() + # si2.submit() + # self._test_recurring_invoice(si2, False) - # yearly - si5 = frappe.copy_doc(base_si) - si5.update({ - "recurring_type": "Yearly", - "period_from": get_first_day(today), - "period_to": get_last_day(add_to_date(today, years=1)) - }) - si5.insert() - si5.submit() - self._test_recurring_invoice(si5, True) + # # quarterly + # si3 = frappe.copy_doc(base_si) + # si3.update({ + # "recurring_type": "Quarterly", + # "period_from": get_first_day(today), + # "period_to": get_last_day(add_to_date(today, months=3)) + # }) + # si3.insert() + # si3.submit() + # self._test_recurring_invoice(si3, True) - # yearly without a first and last day period - si6 = frappe.copy_doc(base_si) - si6.update({ - "recurring_type": "Yearly", - "period_from": today, - "period_to": add_to_date(today, years=1) - }) - si6.insert() - si6.submit() - self._test_recurring_invoice(si6, False) + # # quarterly without a first and last day period + # si4 = frappe.copy_doc(base_si) + # si4.update({ + # "recurring_type": "Quarterly", + # "period_from": today, + # "period_to": add_to_date(today, months=3) + # }) + # si4.insert() + # si4.submit() + # self._test_recurring_invoice(si4, 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() + # # yearly + # si5 = frappe.copy_doc(base_si) + # si5.update({ + # "recurring_type": "Yearly", + # "period_from": get_first_day(today), + # "period_to": get_last_day(add_to_date(today, years=1)) + # }) + # si5.insert() + # si5.submit() + # self._test_recurring_invoice(si5, True) - # setting so that _test function works - si7.posting_date = today - self._test_recurring_invoice(si7, True) + # # yearly without a first and last day period + # si6 = frappe.copy_doc(base_si) + # si6.update({ + # "recurring_type": "Yearly", + # "period_from": today, + # "period_to": add_to_date(today, years=1) + # }) + # si6.insert() + # si6.submit() + # self._test_recurring_invoice(si6, False) - 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 + # # 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() - no_of_months = ({"Monthly": 1, "Quarterly": 3, "Yearly": 12})[base_si.recurring_type] + # # setting so that _test function works + # si7.posting_date = today + # self._test_recurring_invoice(si7, True) - 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]) + # 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 - next_date = get_next_date(base_si.posting_date, no_of_months, - base_si.repeat_on_day_of_month) + # no_of_months = ({"Monthly": 1, "Quarterly": 3, "Yearly": 12})[base_si.recurring_type] - manage_recurring_invoices(next_date=next_date, commit=False) + # 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]) - 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) + # next_date = get_next_date(base_si.posting_date, no_of_months, + # base_si.repeat_on_day_of_month) - self.assertEquals(i+2, len(recurred_invoices)) + # manage_recurring_invoices(next_date=next_date, commit=False) - new_si = frappe.get_doc("Sales Invoice", recurred_invoices[0][0]) + # 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) - 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(i+2, len(recurred_invoices)) - self.assertEquals(new_si.posting_date, unicode(next_date)) + # new_si = frappe.get_doc("Sales Invoice", recurred_invoices[0][0]) - self.assertEquals(new_si.period_from, - unicode(add_months(base_si.period_from, no_of_months))) + # 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)) - if first_and_last_day: - self.assertEquals(new_si.period_to, - unicode(get_last_day(add_months(base_si.period_to, - no_of_months)))) - else: - self.assertEquals(new_si.period_to, - unicode(add_months(base_si.period_to, no_of_months))) + # self.assertEquals(new_si.posting_date, unicode(next_date)) + + # 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.period_to, + # unicode(get_last_day(add_months(base_si.period_to, + # no_of_months)))) + # else: + # self.assertEquals(new_si.period_to, + # unicode(add_months(base_si.period_to, no_of_months))) - return new_si + # 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) + # # 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) 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 d9705c25ec0..4a236736309 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 @@ -428,22 +430,6 @@ class AccountsController(TransactionBase): return stock_items - @property - def company_abbr(self): - if not hasattr(self, "_abbr"): - self._abbr = frappe.db.get_value("Company", self.company, "abbr") - - return self._abbr - - def check_credit_limit(self, account): - total_outstanding = frappe.db.sql(""" - select sum(ifnull(debit, 0)) - sum(ifnull(credit, 0)) - from `tabGL Entry` where account = %s""", account) - - total_outstanding = total_outstanding[0][0] if total_outstanding else 0 - 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() @@ -468,6 +454,22 @@ class AccountsController(TransactionBase): set convert_into_recurring = 0 where recurring_id = %s""" % (self.doctype, '%s'), (self.recurring_id)) + @property + def company_abbr(self): + if not hasattr(self, "_abbr"): + self._abbr = frappe.db.get_value("Company", self.company, "abbr") + + return self._abbr + + def check_credit_limit(self, account): + total_outstanding = frappe.db.sql(""" + select sum(ifnull(debit, 0)) - sum(ifnull(credit, 0)) + from `tabGL Entry` where account = %s""", account) + + total_outstanding = total_outstanding[0][0] if total_outstanding else 0 + if total_outstanding: + frappe.get_doc('Account', account).check_credit_limit(total_outstanding) + def validate_notification_email_id(self): if self.notification_email_address: email_list = filter(None, [cstr(email).strip() for email in @@ -487,6 +489,8 @@ class AccountsController(TransactionBase): """ Set next date on which recurring document will be created""" from erpnext.controllers.recurring_document import get_next_date + month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} + if not self.repeat_on_day_of_month: msgprint(_("Please enter 'Repeat on Day of Month' field value"), raise_exception=1) diff --git a/erpnext/controllers/recurring_document.py b/erpnext/controllers/recurring_document.py index 24e38452051..d3c78096148 100644 --- a/erpnext/controllers/recurring_document.py +++ b/erpnext/controllers/recurring_document.py @@ -11,25 +11,33 @@ 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 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%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)) + 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)): + 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, next_date) + new_document_wrapper = make_new_document(ref_wrapper, date_field, next_date) send_notification(new_document_wrapper) if commit: frappe.db.commit() @@ -39,7 +47,7 @@ def manage_recurring_documents(doctype, next_date=None, commit=True): frappe.db.begin() frappe.db.sql("update `tab%s` \ - set convert_into_recurring = 0 where name = %s", % (doctype, '%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() @@ -53,10 +61,9 @@ def manage_recurring_documents(doctype, next_date=None, commit=True): exception_message = "\n\n".join([cstr(d) for d in exception_list]) frappe.throw(exception_message) -def make_new_document(ref_wrapper, posting_date): +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] period_from = get_next_date(ref_wrapper.period_from, mcount) @@ -73,7 +80,7 @@ def make_new_document(ref_wrapper, posting_date): period_to = get_next_date(ref_wrapper.period_to, mcount) new_document.update({ - "transaction_date": posting_date, + date_field: posting_date, "period_from": period_from, "period_to": period_to, "fiscal_year": get_fiscal_year(posting_date)[0], diff --git a/erpnext/controllers/tests/__init__.py b/erpnext/controllers/tests/__init__.py new file mode 100644 index 00000000000..60bec4fbecd --- /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 00000000000..d31f6324bb8 --- /dev/null +++ b/erpnext/controllers/tests/test_recurring_document.py @@ -0,0 +1,165 @@ +# 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 + today = nowdate() + base_doc = frappe.copy_doc(test_records[0]) + + base_doc.update({ + "convert_into_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], + "period_from": get_first_day(today), + "period_to": 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({ + "period_from": today, + "period_to": 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", + "period_from": get_first_day(today), + "period_to": 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", + "period_from": today, + "period_to": 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", + "period_from": get_first_day(today), + "period_to": 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", + "period_from": today, + "period_to": 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: add_to_date(today, days=-1) + }) + 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 ["convert_into_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.period_from, + unicode(add_months(base_doc.period_from, no_of_months))) + + if first_and_last_day: + obj.assertEquals(new_doc.period_to, + unicode(get_last_day(add_months(base_doc.period_to, + no_of_months)))) + else: + obj.assertEquals(new_doc.period_to, + unicode(add_months(base_doc.period_to, 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) \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 128c5774d9e..c55b7b383fe 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')