mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-06 21:59:13 +00:00
Merge pull request #2109 from ankitjavalkarwork/recurringorder2
Commonified Recurring for Sales Invoice and Sales Order
This commit is contained in:
@@ -228,7 +228,7 @@ cur_frm.cscript.hide_fields = function(doc) {
|
|||||||
par_flds = ['project_name', 'due_date', 'is_opening', 'source', 'total_advance', 'gross_profit',
|
par_flds = ['project_name', 'due_date', 'is_opening', 'source', 'total_advance', 'gross_profit',
|
||||||
'gross_profit_percent', 'get_advances_received',
|
'gross_profit_percent', 'get_advances_received',
|
||||||
'advance_adjustment_details', 'sales_partner', 'commission_rate',
|
'advance_adjustment_details', 'sales_partner', 'commission_rate',
|
||||||
'total_commission', 'advances', 'invoice_period_from_date', 'invoice_period_to_date'];
|
'total_commission', 'advances', 'from_date', 'to_date'];
|
||||||
|
|
||||||
item_flds_normal = ['sales_order', 'delivery_note']
|
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
|
// set default values for recurring invoices
|
||||||
if(doc.convert_into_recurring_invoice) {
|
if(doc.is_recurring) {
|
||||||
var owner_email = doc.owner=="Administrator"
|
var owner_email = doc.owner=="Administrator"
|
||||||
? frappe.user_info("Administrator").email
|
? frappe.user_info("Administrator").email
|
||||||
: doc.owner;
|
: 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"]);
|
refresh_many(["notification_email_address", "repeat_on_day_of_month"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
cur_frm.cscript.invoice_period_from_date = function(doc, dt, dn) {
|
cur_frm.cscript.from_date = function(doc, dt, dn) {
|
||||||
// set invoice_period_to_date
|
// set to_date
|
||||||
if(doc.invoice_period_from_date) {
|
if(doc.from_date) {
|
||||||
var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6,
|
var recurring_type_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6,
|
||||||
'Yearly': 12};
|
'Yearly': 12};
|
||||||
|
|
||||||
var months = recurring_type_map[doc.recurring_type];
|
var months = recurring_type_map[doc.recurring_type];
|
||||||
if(months) {
|
if(months) {
|
||||||
var to_date = frappe.datetime.add_months(doc.invoice_period_from_date,
|
var to_date = frappe.datetime.add_months(doc.from_date,
|
||||||
months);
|
months);
|
||||||
doc.invoice_period_to_date = frappe.datetime.add_days(to_date, -1);
|
doc.to_date = frappe.datetime.add_days(to_date, -1);
|
||||||
refresh_field('invoice_period_to_date');
|
refresh_field('to_date');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"allow_import": 1,
|
"allow_attach": 1,
|
||||||
|
"allow_import": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2013-05-24 19:29:05",
|
"creation": "2013-05-24 19:29:05",
|
||||||
"default_print_format": "Standard",
|
"default_print_format": "Standard",
|
||||||
@@ -172,9 +173,9 @@
|
|||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"depends_on": "",
|
"depends_on": "",
|
||||||
"description": "Start date of current invoice's period",
|
"description": "Start date of current invoice's period",
|
||||||
"fieldname": "invoice_period_from_date",
|
"fieldname": "from_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Invoice Period From",
|
"label": "From",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"print_hide": 0,
|
"print_hide": 0,
|
||||||
@@ -184,9 +185,9 @@
|
|||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"depends_on": "",
|
"depends_on": "",
|
||||||
"description": "End date of current invoice's period",
|
"description": "End date of current invoice's period",
|
||||||
"fieldname": "invoice_period_to_date",
|
"fieldname": "to_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Invoice Period To",
|
"label": "To",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"print_hide": 0,
|
"print_hide": 0,
|
||||||
@@ -1087,9 +1088,9 @@
|
|||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:doc.docstatus<2",
|
"depends_on": "eval:doc.docstatus<2",
|
||||||
"description": "Check if recurring invoice, uncheck to stop recurring or put proper End Date",
|
"description": "Check if recurring invoice, uncheck to stop recurring or put proper End Date",
|
||||||
"fieldname": "convert_into_recurring_invoice",
|
"fieldname": "is_recurring",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Convert into Recurring Invoice",
|
"label": "Is Recurring",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@@ -1097,7 +1098,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
|
"depends_on": "eval:doc.is_recurring==1",
|
||||||
"description": "Select the period when the invoice will be generated automatically",
|
"description": "Select the period when the invoice will be generated automatically",
|
||||||
"fieldname": "recurring_type",
|
"fieldname": "recurring_type",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -1110,7 +1111,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
|
"depends_on": "eval:doc.is_recurring==1",
|
||||||
"description": "The day of the month on which auto invoice will be generated e.g. 05, 28 etc ",
|
"description": "The day of the month on which auto invoice will be generated e.g. 05, 28 etc ",
|
||||||
"fieldname": "repeat_on_day_of_month",
|
"fieldname": "repeat_on_day_of_month",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
@@ -1121,7 +1122,7 @@
|
|||||||
"read_only": 0
|
"read_only": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
|
"depends_on": "eval:doc.is_recurring==1",
|
||||||
"description": "The date on which next invoice will be generated. It is generated on submit.\n",
|
"description": "The date on which next invoice will be generated. It is generated on submit.\n",
|
||||||
"fieldname": "next_date",
|
"fieldname": "next_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
@@ -1133,7 +1134,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
|
"depends_on": "eval:doc.is_recurring==1",
|
||||||
"description": "The date on which recurring invoice will be stop",
|
"description": "The date on which recurring invoice will be stop",
|
||||||
"fieldname": "end_date",
|
"fieldname": "end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
@@ -1153,7 +1154,7 @@
|
|||||||
"width": "50%"
|
"width": "50%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
|
"depends_on": "eval:doc.is_recurring==1",
|
||||||
"description": "The unique id for tracking all recurring invoices.\u00a0It is generated on submit.",
|
"description": "The unique id for tracking all recurring invoices.\u00a0It is generated on submit.",
|
||||||
"fieldname": "recurring_id",
|
"fieldname": "recurring_id",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@@ -1165,7 +1166,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
|
"depends_on": "eval:doc.is_recurring==1",
|
||||||
"description": "Enter email id separated by commas, invoice will be mailed automatically on particular date",
|
"description": "Enter email id separated by commas, invoice will be mailed automatically on particular date",
|
||||||
"fieldname": "notification_email_address",
|
"fieldname": "notification_email_address",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
@@ -1192,7 +1193,7 @@
|
|||||||
"icon": "icon-file-text",
|
"icon": "icon-file-text",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"modified": "2014-08-14 02:13:09.673510",
|
"modified": "2014-08-28 11:21:00.726344",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -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 erpnext.controllers.stock_controller import update_gl_entries_after
|
||||||
from frappe.model.mapper import get_mapped_doc
|
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
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
|
|
||||||
@@ -75,7 +75,7 @@ class SalesInvoice(SellingController):
|
|||||||
self.set_against_income_account()
|
self.set_against_income_account()
|
||||||
self.validate_c_form()
|
self.validate_c_form()
|
||||||
self.validate_time_logs_are_submitted()
|
self.validate_time_logs_are_submitted()
|
||||||
self.validate_recurring_invoice()
|
validate_recurring_document(self)
|
||||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount",
|
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount",
|
||||||
"delivery_note_details")
|
"delivery_note_details")
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
self.update_c_form()
|
self.update_c_form()
|
||||||
self.update_time_log_batch(self.name)
|
self.update_time_log_batch(self.name)
|
||||||
self.convert_to_recurring()
|
convert_to_recurring(self, "RECINV.#####", self.posting_date)
|
||||||
|
|
||||||
def before_cancel(self):
|
def before_cancel(self):
|
||||||
self.update_time_log_batch(None)
|
self.update_time_log_batch(None)
|
||||||
@@ -144,8 +144,8 @@ class SalesInvoice(SellingController):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def on_update_after_submit(self):
|
def on_update_after_submit(self):
|
||||||
self.validate_recurring_invoice()
|
validate_recurring_document(self)
|
||||||
self.convert_to_recurring()
|
convert_to_recurring(self, "RECINV.#####", self.posting_date)
|
||||||
|
|
||||||
def get_portal_page(self):
|
def get_portal_page(self):
|
||||||
return "invoice" if self.docstatus==1 else None
|
return "invoice" if self.docstatus==1 else None
|
||||||
@@ -592,172 +592,6 @@ class SalesInvoice(SellingController):
|
|||||||
grand_total = %s where invoice_no = %s and parent = %s""",
|
grand_total = %s where invoice_no = %s and parent = %s""",
|
||||||
(self.name, self.amended_from, self.c_form_no))
|
(self.name, self.amended_from, self.c_form_no))
|
||||||
|
|
||||||
def validate_recurring_invoice(self):
|
|
||||||
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()
|
@frappe.whitelist()
|
||||||
def get_bank_cash_account(mode_of_payment):
|
def get_bank_cash_account(mode_of_payment):
|
||||||
val = frappe.db.get_value("Mode of Payment", mode_of_payment, "default_account")
|
val = frappe.db.get_value("Mode of Payment", mode_of_payment, "default_account")
|
||||||
|
|||||||
@@ -665,143 +665,9 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
where against_invoice=%s""", si.name))
|
where against_invoice=%s""", si.name))
|
||||||
|
|
||||||
def test_recurring_invoice(self):
|
def test_recurring_invoice(self):
|
||||||
from frappe.utils import get_first_day, get_last_day, add_to_date, nowdate, getdate
|
from erpnext.controllers.tests.test_recurring_document import test_recurring_document
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
# monthly
|
test_recurring_document(self, test_records)
|
||||||
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)
|
|
||||||
|
|
||||||
def clear_stock_account_balance(self):
|
def clear_stock_account_balance(self):
|
||||||
frappe.db.sql("delete from `tabStock Ledger Entry`")
|
frappe.db.sql("delete from `tabStock Ledger Entry`")
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, throw
|
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.setup.utils import get_company_currency, get_exchange_rate
|
||||||
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
@@ -444,7 +446,6 @@ class AccountsController(TransactionBase):
|
|||||||
if total_outstanding:
|
if total_outstanding:
|
||||||
frappe.get_doc('Account', account).check_credit_limit(total_outstanding)
|
frappe.get_doc('Account', account).check_credit_limit(total_outstanding)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_tax_rate(account_head):
|
def get_tax_rate(account_head):
|
||||||
return frappe.db.get_value("Account", account_head, "tax_rate")
|
return frappe.db.get_value("Account", account_head, "tax_rate")
|
||||||
|
|||||||
199
erpnext/controllers/recurring_document.py
Normal file
199
erpnext/controllers/recurring_document.py
Normal 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))
|
||||||
1
erpnext/controllers/tests/__init__.py
Normal file
1
erpnext/controllers/tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from erpnext.__version__ import __version__
|
||||||
166
erpnext/controllers/tests/test_recurring_document.py
Normal file
166
erpnext/controllers/tests/test_recurring_document.py
Normal 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)
|
||||||
@@ -64,7 +64,7 @@ scheduler_events = {
|
|||||||
"erpnext.selling.doctype.lead.get_leads.get_leads"
|
"erpnext.selling.doctype.lead.get_leads.get_leads"
|
||||||
],
|
],
|
||||||
"daily": [
|
"daily": [
|
||||||
"erpnext.accounts.doctype.sales_invoice.sales_invoice.manage_recurring_invoices",
|
"erpnext.controllers.recurring_document.create_recurring_documents"
|
||||||
"erpnext.stock.utils.reorder_item",
|
"erpnext.stock.utils.reorder_item",
|
||||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||||
"erpnext.support.doctype.support_ticket.support_ticket.auto_close_tickets"
|
"erpnext.support.doctype.support_ticket.support_ticket.auto_close_tickets"
|
||||||
|
|||||||
@@ -78,4 +78,8 @@ erpnext.patches.v4_2.update_project_milestones
|
|||||||
erpnext.patches.v4_2.add_currency_turkish_lira #2014-08-08
|
erpnext.patches.v4_2.add_currency_turkish_lira #2014-08-08
|
||||||
execute:frappe.delete_doc("DocType", "Landed Cost Wizard")
|
execute:frappe.delete_doc("DocType", "Landed Cost Wizard")
|
||||||
erpnext.patches.v4_2.default_website_style
|
erpnext.patches.v4_2.default_website_style
|
||||||
|
<<<<<<< HEAD
|
||||||
erpnext.patches.v4_2.set_company_country
|
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
|
||||||
|
|||||||
@@ -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""")
|
||||||
@@ -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() {
|
cur_frm.cscript.send_sms = function() {
|
||||||
frappe.require("assets/erpnext/js/sms_manager.js");
|
frappe.require("assets/erpnext/js/sms_manager.js");
|
||||||
var sms_man = new SMSManager(cur_frm.doc);
|
var sms_man = new SMSManager(cur_frm.doc);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"allow_import": 1,
|
"allow_attach": 1,
|
||||||
|
"allow_import": 1,
|
||||||
"autoname": "naming_series:",
|
"autoname": "naming_series:",
|
||||||
"creation": "2013-06-18 12:39:59",
|
"creation": "2013-06-18 12:39:59",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@@ -169,6 +170,24 @@
|
|||||||
"search_index": 1,
|
"search_index": 1,
|
||||||
"width": "160px"
|
"width": "160px"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"description": "Start date of current order's period",
|
||||||
|
"fieldname": "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",
|
"description": "Customer's Purchase Order Number",
|
||||||
"fieldname": "po_no",
|
"fieldname": "po_no",
|
||||||
@@ -888,13 +907,121 @@
|
|||||||
"options": "Sales Team",
|
"options": "Sales Team",
|
||||||
"permlevel": 0,
|
"permlevel": 0,
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "recurring_order",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Recurring Order",
|
||||||
|
"options": "icon-time",
|
||||||
|
"permlevel": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break82",
|
||||||
|
"fieldtype": "Column Break",
|
||||||
|
"label": "Column Break",
|
||||||
|
"permlevel": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"allow_on_submit": 1,
|
||||||
|
"depends_on": "eval:doc.docstatus<2",
|
||||||
|
"description": "Check if recurring order, uncheck to stop recurring or put proper End Date",
|
||||||
|
"fieldname": "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",
|
"icon": "icon-file-text",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"issingle": 0,
|
"issingle": 0,
|
||||||
"modified": "2014-08-11 07:28:11.362232",
|
"modified": "2014-08-28 11:22:10.959416",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order",
|
"name": "Sales Order",
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from frappe.utils import cstr, flt, getdate, comma_and
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
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
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
|
|
||||||
form_grid_templates = {
|
form_grid_templates = {
|
||||||
@@ -120,6 +122,8 @@ class SalesOrder(SellingController):
|
|||||||
if not self.billing_status: self.billing_status = 'Not Billed'
|
if not self.billing_status: self.billing_status = 'Not Billed'
|
||||||
if not self.delivery_status: self.delivery_status = 'Not Delivered'
|
if not self.delivery_status: self.delivery_status = 'Not Delivered'
|
||||||
|
|
||||||
|
validate_recurring_document(self)
|
||||||
|
|
||||||
def validate_warehouse(self):
|
def validate_warehouse(self):
|
||||||
from erpnext.stock.utils import validate_warehouse_company
|
from erpnext.stock.utils import validate_warehouse_company
|
||||||
|
|
||||||
@@ -162,6 +166,8 @@ class SalesOrder(SellingController):
|
|||||||
self.update_prevdoc_status('submit')
|
self.update_prevdoc_status('submit')
|
||||||
frappe.db.set(self, 'status', 'Submitted')
|
frappe.db.set(self, 'status', 'Submitted')
|
||||||
|
|
||||||
|
convert_to_recurring(self, "SO/REC/.#####", self.transaction_date)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
# Cannot cancel stopped SO
|
# Cannot cancel stopped SO
|
||||||
if self.status == 'Stopped':
|
if self.status == 'Stopped':
|
||||||
@@ -249,6 +255,10 @@ class SalesOrder(SellingController):
|
|||||||
def get_portal_page(self):
|
def get_portal_page(self):
|
||||||
return "order" if self.docstatus==1 else None
|
return "order" if self.docstatus==1 else None
|
||||||
|
|
||||||
|
def on_update_after_submit(self):
|
||||||
|
validate_recurring_document(self)
|
||||||
|
convert_to_recurring(self, "SO/REC/.#####", self.transaction_date)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_material_request(source_name, target_doc=None):
|
def make_material_request(source_name, target_doc=None):
|
||||||
|
|||||||
@@ -331,6 +331,11 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.CancelledLinkError, delivery_note.submit)
|
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_dependencies = ["Sales BOM", "Currency Exchange"]
|
||||||
|
|
||||||
test_records = frappe.get_test_records('Sales Order')
|
test_records = frappe.get_test_records('Sales Order')
|
||||||
|
|||||||
12
erpnext/templates/emails/recurring_document_failed.html
Normal file
12
erpnext/templates/emails/recurring_document_failed.html
Normal 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>
|
||||||
@@ -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>
|
|
||||||
Reference in New Issue
Block a user