Merge pull request #2109 from ankitjavalkarwork/recurringorder2

Commonified Recurring for Sales Invoice and Sales Order
This commit is contained in:
Anand Doshi 2014-09-01 16:34:12 +05:30
commit 4228adfaa7
17 changed files with 598 additions and 347 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',
'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');
}
}
}

View File

@ -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",

View File

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

View File

@ -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`")

View File

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

View File

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

View File

@ -0,0 +1 @@
from erpnext.__version__ import __version__

View File

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

View File

@ -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"

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

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

View File

@ -0,0 +1,12 @@
<h2>Recurring {{ type }} Failed</h2>
<p>An error occured while creating recurring {{ type }} <b>{{ name }}</b> for <b>{{ customer }}</b>.</p>
<p>This could be because of some invalid email ids in the {{ type }}.</p>
<p>To stop sending repetitive error notifications from the system, we have unchecked
"Convert into Recurring" field in the {{ type }} {{ name }}.</p>
<p><b>Please correct the {{ type }} and make the {{ type }} recurring again.</b></p>
<hr>
<p><b>It is necessary to take this action today itself for the above mentioned recurring {{ 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 }}.</b></p>
<p>[This email is autogenerated]</p>

View File

@ -1,12 +0,0 @@
<h2>Recurring Invoice Failed</h2>
<p>An error occured while creating recurring invoice <b>{{ name }}</b> for <b>{{ customer }}</b>.</p>
<p>This could be because of some invalid email ids in the invoice.</p>
<p>To stop sending repetitive error notifications from the system, we have unchecked
"Convert into Recurring" field in the invoice {{ name }}.</p>
<p><b>Please correct the invoice and make the invoice recurring again.</b></p>
<hr>
<p><b>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.</b></p>
<p>[This email is autogenerated]</p>