Merge branch 'master' of github.com:webnotes/erpnext
diff --git a/accounts/doctype/account/account.py b/accounts/doctype/account/account.py
index ed374b1..350ebbe 100644
--- a/accounts/doctype/account/account.py
+++ b/accounts/doctype/account/account.py
@@ -49,11 +49,6 @@
msgprint("Message: Please enter Master Name once the account is created.")
- # Rate is mandatory for tax account
- def validate_rate_for_tax(self):
- if self.doc.account_type == 'Tax' and not self.doc.tax_rate:
- msgprint("Please Enter Rate", raise_exception=1)
-
# Fetch Parent Details and validation for account not to be created under ledger
def validate_parent(self):
if self.doc.parent_account:
@@ -135,7 +130,6 @@
def validate(self):
self.validate_master_name()
- self.validate_rate_for_tax()
self.validate_parent()
self.validate_duplicate_account()
self.validate_root_details()
diff --git a/accounts/doctype/gl_control/gl_control.py b/accounts/doctype/gl_control/gl_control.py
index 6f04538..358e5f0 100644
--- a/accounts/doctype/gl_control/gl_control.py
+++ b/accounts/doctype/gl_control/gl_control.py
@@ -21,7 +21,7 @@
from webnotes.utils import add_days, add_months, add_years, cint, cstr, date_diff, default_fields, flt, fmt_money, formatdate, getTraceback, get_defaults, get_first_day, get_last_day, getdate, has_common, month_name, now, nowdate, replace_newlines, sendmail, set_default, str_esc_quote, user_format, validate_email_add
from webnotes.model import db_exists
from webnotes.model.doc import Document, addchild, getchildren, make_autoname
-from webnotes.model.wrapper import getlist, copy_doclist, clone
+from webnotes.model.wrapper import getlist, copy_doclist
from webnotes.model.code import get_obj
from webnotes import session, form, msgprint, errprint
from webnotes.utils.email_lib import sendmail
@@ -397,194 +397,3 @@
fy_obj = get_obj('Fiscal Year', fy[0])
for a in set(ac_list):
fy_obj.repost(a)
-
-
-def manage_recurring_invoices():
- """
- Create recurring invoices on specific date by copying the original one
- and notify the concerned people
- """
- rv = webnotes.conn.sql("""select name, recurring_id from `tabSales Invoice` \
- where ifnull(convert_into_recurring_invoice, 0) = 1 and next_date = %s \
- and next_date <= ifnull(end_date, '2199-12-31') and docstatus=1""", nowdate())
-
-
- exception_list = []
- for d in rv:
- if not webnotes.conn.sql("""select name from `tabSales Invoice` \
- where posting_date = %s and recurring_id = %s and docstatus=1""", (nowdate(), d[1])):
- try:
- prev_rv = get_obj('Sales Invoice', d[0], with_children=1)
- new_rv = create_new_invoice(prev_rv)
-
- send_notification(new_rv)
- webnotes.conn.commit()
- except Exception, e:
- webnotes.conn.rollback()
-
- webnotes.conn.begin()
- webnotes.conn.sql("update `tabSales Invoice` set \
- convert_into_recurring_invoice = 0 where name = %s", d[0])
- notify_errors(d[0], prev_rv.doc.owner)
- webnotes.conn.commit()
-
- exception_list.append(e)
- finally:
- webnotes.conn.begin()
-
- if exception_list:
- exception_message = "\n\n".join([cstr(d) for d in exception_list])
- raise Exception, exception_message
-
-
-def notify_errors(inv, owner):
- import webnotes
- import website
-
- exception_msg = """
- Dear User,
-
- An error occured while creating recurring invoice from %s (at %s).
-
- May be there are some invalid email ids mentioned in the invoice.
-
- To stop sending repetitive error notifications from the system, we have unchecked
- "Convert into Recurring" field in the invoice %s.
-
-
- Please correct the invoice and make the invoice recurring again.
-
- <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>
-
- Regards,
- Administrator
-
- """ % (inv, website.get_site_address(), inv)
- subj = "[Urgent] Error while creating recurring invoice from %s" % inv
-
- from webnotes.profile import get_system_managers
- recipients = get_system_managers()
- owner_email = webnotes.conn.get_value("Profile", owner, "email")
- if not owner_email in recipients:
- recipients.append(owner_email)
-
- assign_task_to_owner(inv, exception_msg, recipients)
- sendmail(recipients, subject=subj, msg = exception_msg)
-
-
-
-def assign_task_to_owner(inv, msg, users):
- for d in users:
- if d.lower() == 'administrator':
- d = webnotes.conn.sql("select ifnull(email_id, '') \
- from `tabProfile` where name = 'Administrator'")[0][0]
- from webnotes.widgets.form import assign_to
- args = {
- 'assign_to' : d,
- 'doctype' : 'Sales Invoice',
- 'name' : inv,
- 'description' : msg,
- 'priority' : 'Urgent'
- }
- assign_to.add(args)
-
-
-def create_new_invoice(prev_rv):
- # clone rv
- new_rv = clone(prev_rv)
-
- mdict = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
- mcount = mdict[prev_rv.doc.recurring_type]
-
- # update new rv
-
- new_rv.doc.posting_date = new_rv.doc.next_date
- new_rv.doc.aging_date = new_rv.doc.next_date
- new_rv.doc.due_date = add_days(new_rv.doc.next_date, cint(date_diff(prev_rv.doc.due_date, prev_rv.doc.posting_date)))
- new_rv.doc.invoice_period_from_date = get_next_date(new_rv.doc.invoice_period_from_date, mcount)
- new_rv.doc.invoice_period_to_date = get_next_date(new_rv.doc.invoice_period_to_date, mcount)
- new_rv.doc.owner = prev_rv.doc.owner
- new_rv.doc.save()
-
- # submit and after submit
- new_rv.submit()
- new_rv.update_after_submit()
-
- return new_rv
-
-def get_next_date(dt, mcount):
- import datetime
- m = getdate(dt).month + mcount
- y = getdate(dt).year
- d = getdate(dt).day
- if m > 12:
- m, y = m-12, y+1
- try:
- next_month_date = datetime.date(y, m, d)
- except:
- import calendar
- last_day = calendar.monthrange(y, m)[1]
- next_month_date = datetime.date(y, m, last_day)
- return next_month_date.strftime("%Y-%m-%d")
-
-
-def send_notification(new_rv):
- """Notify concerned persons about recurring invoice generation"""
- subject = "Invoice : " + new_rv.doc.name
-
- com = new_rv.doc.company # webnotes.conn.get_value('Control Panel', '', 'letter_head')
-
- hd = '''<div><h2>%s</h2></div>
- <div><h3>Invoice: %s</h3></div>
- <table cellspacing= "5" cellpadding="5" width = "100%%">
- <tr>
- <td width = "50%%"><b>Customer</b><br>%s<br>%s</td>
- <td width = "50%%">Invoice Date : %s<br>Invoice Period : %s to %s <br>Due Date : %s</td>
- </tr>
- </table>
- ''' % (com, new_rv.doc.name, new_rv.doc.customer_name, new_rv.doc.address_display, getdate(new_rv.doc.posting_date).strftime("%d-%m-%Y"), \
- getdate(new_rv.doc.invoice_period_from_date).strftime("%d-%m-%Y"), getdate(new_rv.doc.invoice_period_to_date).strftime("%d-%m-%Y"),\
- getdate(new_rv.doc.due_date).strftime("%d-%m-%Y"))
-
-
- tbl = '''<table border="1px solid #CCC" width="100%%" cellpadding="0px" cellspacing="0px">
- <tr>
- <td width = "15%%" bgcolor="#CCC" align="left"><b>Item</b></td>
- <td width = "40%%" bgcolor="#CCC" align="left"><b>Description</b></td>
- <td width = "15%%" bgcolor="#CCC" align="center"><b>Qty</b></td>
- <td width = "15%%" bgcolor="#CCC" align="center"><b>Rate</b></td>
- <td width = "15%%" bgcolor="#CCC" align="center"><b>Amount</b></td>
- </tr>
- '''
- for d in getlist(new_rv.doclist, 'entries'):
- tbl += '<tr><td>' + d.item_code +'</td><td>' + d.description+'</td><td>' + cstr(d.qty) +'</td><td>' + cstr(d.basic_rate) +'</td><td>' + cstr(d.amount) +'</td></tr>'
- tbl += '</table>'
-
- totals =''' <table cellspacing= "5" cellpadding="5" width = "100%%">
- <tr>
- <td width = "50%%"></td>
- <td width = "50%%">
- <table width = "100%%">
- <tr>
- <td width = "50%%">Net Total: </td><td>%s </td>
- </tr><tr>
- <td width = "50%%">Total Tax: </td><td>%s </td>
- </tr><tr>
- <td width = "50%%">Grand Total: </td><td>%s</td>
- </tr><tr>
- <td width = "50%%">In Words: </td><td>%s</td>
- </tr>
- </table>
- </td>
- </tr>
- <tr><td>Terms and Conditions:</td></tr>
- <tr><td>%s</td></tr>
- </table>
- ''' % (new_rv.doc.net_total, new_rv.doc.other_charges_total,new_rv.doc.grand_total, new_rv.doc.in_words,new_rv.doc.terms)
-
-
- msg = hd + tbl + totals
- recipients = new_rv.doc.notification_email_address.replace('\n', '').replace(' ', '').split(",")
- sendmail(recipients, subject=subject, msg = msg)
diff --git a/accounts/doctype/sales_invoice/sales_invoice.js b/accounts/doctype/sales_invoice/sales_invoice.js
index 87d6f1c..e763440 100644
--- a/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/accounts/doctype/sales_invoice/sales_invoice.js
@@ -93,8 +93,6 @@
for(f in item_flds_normal) cur_frm.fields_dict['entries'].grid.set_column_disp(item_flds_normal[f], true);
for(f in item_flds_pos) cur_frm.fields_dict['entries'].grid.set_column_disp(item_flds_pos[f], false);
}
- if (doc.docstatus==1) unhide_field('recurring_invoice');
- else hide_field('recurring_invoice');
if(doc.customer) unhide_field('contact_section');
else hide_field('contact_section');
@@ -498,23 +496,34 @@
wn.set_route('Report', 'GL Entry', 'General Ledger', 'Voucher No='+cur_frm.doc.name);
}
-// Default values for recurring invoices
-cur_frm.cscript.convert_into_recurring_invoice = function(doc, dt, dn) {
- if (doc.convert_into_recurring_invoice)
- get_server_fields('set_default_recurring_values','','',doc, dt, dn, 0);
-}
-
cur_frm.cscript.on_submit = function(doc, cdt, cdn) {
if(cint(wn.boot.notification_settings.sales_invoice)) {
cur_frm.email_doc(wn.boot.notification_settings.sales_invoice);
}
}
-cur_frm.cscript.invoice_period_from_date = function(doc, dt, dn) {
- if(doc.invoice_period_from_date) {
- var recurring_type_map = { 'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12 };
+cur_frm.cscript.convert_into_recurring_invoice = function(doc, dt, dn) {
+ // set default values for recurring invoices
+ if(doc.convert_into_recurring_invoice) {
+ var owner_email = doc.owner=="Administrator"
+ ? wn.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 = wn.datetime.str_to_obj(doc.posting_date).getDate();
+ }
+
+ refresh_many(["notification_email_address", "repeat_on_day_of_month"]);
+}
- var months = $(recurring_type_map).attr(doc.recurring_type);
+cur_frm.cscript.invoice_period_from_date = function(doc, dt, dn) {
+ // set invoice_period_to_date
+ if(doc.invoice_period_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 = wn.datetime.add_months(doc.invoice_period_from_date,
months);
@@ -522,4 +531,4 @@
refresh_field('invoice_period_to_date');
}
}
-}
\ No newline at end of file
+}
diff --git a/accounts/doctype/sales_invoice/sales_invoice.py b/accounts/doctype/sales_invoice/sales_invoice.py
index aa2e52b..2aa30fd 100644
--- a/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/accounts/doctype/sales_invoice/sales_invoice.py
@@ -19,6 +19,9 @@
import webnotes
from webnotes.utils import add_days, add_months, add_years, cint, cstr,date_diff, default_fields, flt, fmt_money, formatdate,getTraceback, get_defaults, get_first_day, get_last_day, getdate, has_common,month_name, now, nowdate, replace_newlines, sendmail, set_default,str_esc_quote, user_format, validate_email_add
+
+from webnotes.utils import comma_and
+from webnotes import _
from webnotes.model import db_exists
from webnotes.model.doc import Document, addchild, getchildren, make_autoname
from webnotes.model.wrapper import getlist, copy_doclist
@@ -29,6 +32,8 @@
convert_to_lists = webnotes.conn.convert_to_lists
session = webnotes.session
+month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
+
# -----------------------------------------------------------------------------------------
from utilities.transaction_base import TransactionBase
@@ -42,6 +47,90 @@
def autoname(self):
self.doc.name = make_autoname(self.doc.naming_series+ '.#####')
+
+ def validate(self):
+ self.so_dn_required()
+ self.validate_proj_cust()
+ sales_com_obj = get_obj('Sales Common')
+ sales_com_obj.check_stop_sales_order(self)
+ sales_com_obj.check_active_sales_items(self)
+ sales_com_obj.check_conversion_rate(self)
+ sales_com_obj.validate_max_discount(self, 'entries') #verify whether rate is not greater than tolerance
+ sales_com_obj.get_allocated_sum(self) # this is to verify that the allocated % of sales persons is 100%
+ sales_com_obj.validate_fiscal_year(self.doc.fiscal_year,self.doc.posting_date,'Posting Date')
+ self.validate_customer()
+ self.validate_customer_account()
+ self.validate_debit_acc()
+ self.validate_fixed_asset_account()
+ self.add_remarks()
+ if cint(self.doc.is_pos):
+ self.validate_pos()
+ self.validate_write_off_account()
+ if cint(self.doc.update_stock):
+ sl = get_obj('Stock Ledger')
+ sl.validate_serial_no(self, 'entries')
+ sl.validate_serial_no(self, 'packing_details')
+ self.validate_item_code()
+ self.update_current_stock()
+ self.validate_delivery_note()
+ self.set_in_words()
+ if not self.doc.is_opening:
+ self.doc.is_opening = 'No'
+ self.set_aging_date()
+ self.clear_advances()
+ self.set_against_income_account()
+ self.validate_c_form()
+ self.validate_recurring_invoice()
+
+ def on_submit(self):
+ if cint(self.doc.is_pos) == 1:
+ if cint(self.doc.update_stock) == 1:
+ sl_obj = get_obj("Stock Ledger")
+ sl_obj.validate_serial_no_warehouse(self, 'entries')
+ sl_obj.validate_serial_no_warehouse(self, 'packing_details')
+
+ sl_obj.update_serial_record(self, 'entries', is_submit = 1, is_incoming = 0)
+ sl_obj.update_serial_record(self, 'packing_details', is_submit = 1, is_incoming = 0)
+
+ self.update_stock_ledger(update_stock=1)
+ else:
+ # Check for Approving Authority
+ if not self.doc.recurring_id:
+ get_obj('Authorization Control').validate_approving_authority(self.doc.doctype, self.doc.company, self.doc.grand_total, self)
+
+ self.check_prev_docstatus()
+ get_obj("Sales Common").update_prevdoc_detail(1,self)
+
+ # this sequence because outstanding may get -ve
+ self.make_gl_entries()
+
+ if not cint(self.doc.is_pos) == 1:
+ self.update_against_document_in_jv()
+
+ self.update_c_form()
+
+ self.convert_to_recurring()
+
+
+ def on_cancel(self):
+ if cint(self.doc.is_pos) == 1:
+ if cint(self.doc.update_stock) == 1:
+ sl = get_obj('Stock Ledger')
+ sl.update_serial_record(self, 'entries', is_submit = 0, is_incoming = 0)
+ sl.update_serial_record(self, 'packing_details', is_submit = 0, is_incoming = 0)
+
+ self.update_stock_ledger(update_stock = -1)
+
+ sales_com_obj = get_obj(dt = 'Sales Common')
+ sales_com_obj.check_stop_sales_order(self)
+ self.check_next_docstatus()
+ sales_com_obj.update_prevdoc_detail(0, self)
+
+ self.make_gl_entries(is_cancel=1)
+
+ def on_update_after_submit(self):
+ self.validate_recurring_invoice()
+ self.convert_to_recurring()
def set_pos_fields(self):
"""Set retail related fields from pos settings"""
@@ -426,40 +515,6 @@
d.projected_qty = bin and flt(bin[0]['projected_qty']) or 0
- def validate(self):
- self.so_dn_required()
- self.validate_proj_cust()
- sales_com_obj = get_obj('Sales Common')
- sales_com_obj.check_stop_sales_order(self)
- sales_com_obj.check_active_sales_items(self)
- sales_com_obj.check_conversion_rate(self)
- sales_com_obj.validate_max_discount(self, 'entries') #verify whether rate is not greater than tolerance
- sales_com_obj.get_allocated_sum(self) # this is to verify that the allocated % of sales persons is 100%
- sales_com_obj.validate_fiscal_year(self.doc.fiscal_year,self.doc.posting_date,'Posting Date')
- self.validate_customer()
- self.validate_customer_account()
- self.validate_debit_acc()
- self.validate_fixed_asset_account()
- self.add_remarks()
- if cint(self.doc.is_pos):
- self.validate_pos()
- self.validate_write_off_account()
- if cint(self.doc.update_stock):
- sl = get_obj('Stock Ledger')
- sl.validate_serial_no(self, 'entries')
- sl.validate_serial_no(self, 'packing_details')
- self.validate_item_code()
- self.update_current_stock()
- self.validate_delivery_note()
- self.set_in_words()
- if not self.doc.is_opening:
- self.doc.is_opening = 'No'
- self.set_aging_date()
- self.clear_advances()
- self.set_against_income_account()
- self.validate_c_form()
-
-
def get_warehouse(self):
w = webnotes.conn.sql("select warehouse from `tabPOS Setting` where ifnull(user,'') = '%s' and company = '%s'" % (session['user'], self.doc.company))
w = w and w[0][0] or ''
@@ -596,114 +651,255 @@
if submit_jv:
msgprint("Journal Voucher : " + cstr(submit_jv[0][0]) + " has been created against " + cstr(self.doc.doctype) + ". So " + cstr(self.doc.doctype) + " cannot be Cancelled.")
raise Exception, "Validation Error."
-
-
- def on_submit(self):
- if cint(self.doc.is_pos) == 1:
- if cint(self.doc.update_stock) == 1:
- sl_obj = get_obj("Stock Ledger")
- sl_obj.validate_serial_no_warehouse(self, 'entries')
- sl_obj.validate_serial_no_warehouse(self, 'packing_details')
-
- sl_obj.update_serial_record(self, 'entries', is_submit = 1, is_incoming = 0)
- sl_obj.update_serial_record(self, 'packing_details', is_submit = 1, is_incoming = 0)
-
- self.update_stock_ledger(update_stock=1)
- else:
- # Check for Approving Authority
+
+ @property
+ def meta(self):
+ if not hasattr(self, "_meta"):
+ self._meta = webnotes.get_doctype(self.doc.doctype)
+ return self._meta
+
+ def validate_recurring_invoice(self):
+ if self.doc.convert_into_recurring_invoice:
+ self.validate_notification_email_id()
+
+ if not self.doc.recurring_type:
+ msgprint(_("Please select: ") + self.meta.get_label("recurring_type"),
+ raise_exception=1)
+
+ elif not (self.doc.invoice_period_from_date and \
+ self.doc.invoice_period_to_date):
+ msgprint(comma_and([self.meta.get_label("invoice_period_from_date"),
+ self.meta.get_label("invoice_period_to_date")])
+ + _(": Mandatory for a Recurring Invoice."),
+ raise_exception=True)
+
+ def convert_to_recurring(self):
+ if self.doc.convert_into_recurring_invoice:
if not self.doc.recurring_id:
- get_obj('Authorization Control').validate_approving_authority(self.doc.doctype, self.doc.company, self.doc.grand_total, self)
+ webnotes.conn.set(self.doc, "recurring_id",
+ make_autoname("RECINV/.#####"))
+
+ self.set_next_date()
- self.check_prev_docstatus()
- get_obj("Sales Common").update_prevdoc_detail(1,self)
-
- # this sequence because outstanding may get -ve
- self.make_gl_entries()
-
- if not cint(self.doc.is_pos) == 1:
- self.update_against_document_in_jv()
-
- self.update_c_form()
-
-
- def on_cancel(self):
- if cint(self.doc.is_pos) == 1:
- if cint(self.doc.update_stock) == 1:
- sl = get_obj('Stock Ledger')
- sl.update_serial_record(self, 'entries', is_submit = 0, is_incoming = 0)
- sl.update_serial_record(self, 'packing_details', is_submit = 0, is_incoming = 0)
-
- self.update_stock_ledger(update_stock = -1)
-
- sales_com_obj = get_obj(dt = 'Sales Common')
- sales_com_obj.check_stop_sales_order(self)
- self.check_next_docstatus()
- sales_com_obj.update_prevdoc_detail(0, self)
-
- self.make_gl_entries(is_cancel=1)
-
- def set_default_recurring_values(self):
- from webnotes.utils import cstr
-
- owner_email = self.doc.owner
- if owner_email.lower() == 'administrator':
- owner_email = cstr(webnotes.conn.get_value("Profile", "Administrator", "email"))
-
- ret = {
- 'repeat_on_day_of_month' : getdate(self.doc.posting_date).day,
- 'notification_email_address' : ', '.join([owner_email, cstr(self.doc.contact_email)]),
- }
- return ret
-
+ elif self.doc.recurring_id:
+ webnotes.conn.sql("""update `tabSales Invoice`
+ set convert_into_recurring_invoice = 0
+ where recurring_id = %s""", (self.doc.recurring_id,))
+
def validate_notification_email_id(self):
if self.doc.notification_email_address:
+ email_list = filter(None, [cstr(email).strip() for email in
+ self.doc.notification_email_address.replace("\n", "").split(",")])
+
from webnotes.utils import validate_email_add
- for add in self.doc.notification_email_address.replace('\n', '').replace(' ', '').split(","):
- if add and not validate_email_add(add):
- msgprint("%s is not a valid email address" % add, raise_exception=1)
+ for email in email_list:
+ if not validate_email_add(email):
+ msgprint(self.meta.get_label("notification_email_address") \
+ + " - " + _("Invalid Email Address") + ": \"%s\"" % email,
+ raise_exception=1)
+
else:
msgprint("Notification Email Addresses not specified for recurring invoice",
raise_exception=1)
-
-
- def on_update_after_submit(self):
- self.convert_into_recurring()
-
-
- def convert_into_recurring(self):
- if self.doc.convert_into_recurring_invoice:
- self.validate_notification_email_id()
-
- if not self.doc.recurring_type:
- msgprint("Please select recurring type", raise_exception=1)
- elif not self.doc.invoice_period_from_date or not self.doc.invoice_period_to_date:
- msgprint("Invoice period from date and to date is mandatory for recurring invoice", raise_exception=1)
- self.set_next_date()
- if not self.doc.recurring_id:
- webnotes.conn.set(self.doc, 'recurring_id', make_autoname('RECINV/.#####'))
- elif self.doc.recurring_id:
- webnotes.conn.sql("""update `tabSales Invoice` set convert_into_recurring_invoice = 0 where recurring_id = %s""", self.doc.recurring_id)
-
+
def set_next_date(self):
""" Set next date on which auto invoice will be created"""
-
if not self.doc.repeat_on_day_of_month:
- msgprint("""Please enter 'Repeat on Day of Month' field value. \nThe day of the month on which auto invoice
- will be generated e.g. 05, 28 etc.""", raise_exception=1)
-
- import datetime
- mcount = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
- m = getdate(self.doc.posting_date).month + mcount[self.doc.recurring_type]
- y = getdate(self.doc.posting_date).year
- if m > 12:
- m, y = m-12, y+1
- try:
- next_date = datetime.date(y, m, cint(self.doc.repeat_on_day_of_month))
- except:
- import calendar
- last_day = calendar.monthrange(y, m)[1]
- next_date = datetime.date(y, m, last_day)
- next_date = next_date.strftime("%Y-%m-%d")
-
+ msgprint("""Please enter 'Repeat on Day of Month' field value.
+ The day of the month on which auto invoice
+ will be generated e.g. 05, 28 etc.""", raise_exception=1)
+
+ next_date = get_next_date(self.doc.posting_date,
+ month_map[self.doc.recurring_type], cint(self.doc.repeat_on_day_of_month))
webnotes.conn.set(self.doc, 'next_date', next_date)
+
+def get_next_date(dt, mcount, day=None):
+ import datetime
+ month = getdate(dt).month + mcount
+ year = getdate(dt).year
+ if not day:
+ day = getdate(dt).day
+ if month > 12:
+ month, year = m-12, y+1
+ try:
+ next_month_date = datetime.date(year, month, day)
+ except:
+ import calendar
+ last_day = calendar.monthrange(year, month)[1]
+ next_month_date = datetime.date(year, month, last_day)
+ return next_month_date.strftime("%Y-%m-%d")
+
+def manage_recurring_invoices():
+ """
+ Create recurring invoices on specific date by copying the original one
+ and notify the concerned people
+ """
+ recurring_invoices = webnotes.conn.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')""", nowdate())
+
+ exception_list = []
+ for ref_invoice, recurring_id in recurring_invoices:
+ if not webnotes.conn.sql("""select name from `tabSales Invoice`
+ where posting_date=%s and recurring_id=%s and docstatus=1""",
+ (nowdate(), recurring_id)):
+ try:
+ ref_wrapper = webnotes.model_wrapper('Sales Invoice', ref_invoice)
+ new_invoice_wrapper = make_new_invoice(ref_wrapper)
+ send_notification(new_invoice_wrapper)
+ webnotes.conn.commit()
+ except Exception, e:
+ webnotes.conn.rollback()
+
+ webnotes.conn.begin()
+ webnotes.conn.sql("update `tabSales Invoice` set \
+ convert_into_recurring_invoice = 0 where name = %s", ref_invoice)
+ notify_errors(ref_invoice, ref_wrapper.doc.owner)
+ webnotes.conn.commit()
+
+ exception_list.append(webnotes.getTraceback())
+ finally:
+ webnotes.conn.begin()
+
+ if exception_list:
+ exception_message = "\n\n".join([cstr(d) for d in exception_list])
+ raise Exception, exception_message
+
+def make_new_invoice(ref_wrapper):
+ from webnotes.model.wrapper import clone
+ new_invoice = clone(ref_wrapper)
+
+ mcount = month_map[ref_wrapper.doc.recurring_type]
+
+ today = nowdate()
+
+ new_invoice.doc.fields.update({
+ "posting_date": today,
+ "aging_date": today,
+
+ "due_date": add_days(today, cint(date_diff(ref_wrapper.doc.due_date,
+ ref_wrapper.doc.posting_date))),
+
+ "invoice_period_from_date": \
+ get_next_date(ref_wrapper.doc.invoice_period_from_date, mcount),
+
+ "invoice_period_to_date": \
+ get_next_date(ref_wrapper.doc.invoice_period_to_date, mcount),
+
+ "owner": ref_wrapper.doc.owner,
+ })
+
+ new_invoice.submit()
+
+ return new_invoice
+
+def send_notification(new_rv):
+ """Notify concerned persons about recurring invoice generation"""
+ subject = "Invoice : " + new_rv.doc.name
+
+ com = new_rv.doc.company # webnotes.conn.get_value('Control Panel', '', 'letter_head')
+
+ hd = '''<div><h2>%s</h2></div>
+ <div><h3>Invoice: %s</h3></div>
+ <table cellspacing= "5" cellpadding="5" width = "100%%">
+ <tr>
+ <td width = "50%%"><b>Customer</b><br>%s<br>%s</td>
+ <td width = "50%%">Invoice Date : %s<br>Invoice Period : %s to %s <br>Due Date : %s</td>
+ </tr>
+ </table>
+ ''' % (com, new_rv.doc.name, new_rv.doc.customer_name, new_rv.doc.address_display, getdate(new_rv.doc.posting_date).strftime("%d-%m-%Y"), \
+ getdate(new_rv.doc.invoice_period_from_date).strftime("%d-%m-%Y"), getdate(new_rv.doc.invoice_period_to_date).strftime("%d-%m-%Y"),\
+ getdate(new_rv.doc.due_date).strftime("%d-%m-%Y"))
+
+
+ tbl = '''<table border="1px solid #CCC" width="100%%" cellpadding="0px" cellspacing="0px">
+ <tr>
+ <td width = "15%%" bgcolor="#CCC" align="left"><b>Item</b></td>
+ <td width = "40%%" bgcolor="#CCC" align="left"><b>Description</b></td>
+ <td width = "15%%" bgcolor="#CCC" align="center"><b>Qty</b></td>
+ <td width = "15%%" bgcolor="#CCC" align="center"><b>Rate</b></td>
+ <td width = "15%%" bgcolor="#CCC" align="center"><b>Amount</b></td>
+ </tr>
+ '''
+ for d in getlist(new_rv.doclist, 'entries'):
+ tbl += '<tr><td>' + d.item_code +'</td><td>' + d.description+'</td><td>' + cstr(d.qty) +'</td><td>' + cstr(d.basic_rate) +'</td><td>' + cstr(d.amount) +'</td></tr>'
+ tbl += '</table>'
+
+ totals ='''<table cellspacing= "5" cellpadding="5" width = "100%%">
+ <tr>
+ <td width = "50%%"></td>
+ <td width = "50%%">
+ <table width = "100%%">
+ <tr>
+ <td width = "50%%">Net Total: </td><td>%s </td>
+ </tr><tr>
+ <td width = "50%%">Total Tax: </td><td>%s </td>
+ </tr><tr>
+ <td width = "50%%">Grand Total: </td><td>%s</td>
+ </tr><tr>
+ <td width = "50%%">In Words: </td><td>%s</td>
+ </tr>
+ </table>
+ </td>
+ </tr>
+ <tr><td>Terms and Conditions:</td></tr>
+ <tr><td>%s</td></tr>
+ </table>
+ ''' % (new_rv.doc.net_total,
+ new_rv.doc.other_charges_total,new_rv.doc.grand_total,
+ new_rv.doc.in_words,new_rv.doc.terms)
+
+
+ msg = hd + tbl + totals
+
+ sendmail(new_rv.doc.notification_email_address, subject=subject, msg = msg)
+
+def notify_errors(inv, owner):
+ import webnotes
+ import website
+
+ exception_msg = """
+ Dear User,
+
+ An error occured while creating recurring invoice from %s (at %s).
+
+ May be there are some invalid email ids mentioned in the invoice.
+
+ To stop sending repetitive error notifications from the system, we have unchecked
+ "Convert into Recurring" field in the invoice %s.
+
+
+ Please correct the invoice and make the invoice recurring again.
+
+ <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>
+
+ Regards,
+ Administrator
+
+ """ % (inv, website.get_site_address(), inv)
+ subj = "[Urgent] Error while creating recurring invoice from %s" % inv
+
+ from webnotes.profile import get_system_managers
+ recipients = get_system_managers()
+ owner_email = webnotes.conn.get_value("Profile", owner, "email")
+ if not owner_email in recipients:
+ recipients.append(owner_email)
+
+ assign_task_to_owner(inv, exception_msg, recipients)
+ sendmail(recipients, subject=subj, msg = exception_msg)
+
+def assign_task_to_owner(inv, msg, users):
+ for d in users:
+ from webnotes.widgets.form import assign_to
+ args = {
+ 'assign_to' : d,
+ 'doctype' : 'Sales Invoice',
+ 'name' : inv,
+ 'description' : msg,
+ 'priority' : 'Urgent'
+ }
+ assign_to.add(args)
diff --git a/accounts/doctype/sales_invoice/sales_invoice.txt b/accounts/doctype/sales_invoice/sales_invoice.txt
index 4790bc6..2b968c6 100644
--- a/accounts/doctype/sales_invoice/sales_invoice.txt
+++ b/accounts/doctype/sales_invoice/sales_invoice.txt
@@ -2,12 +2,13 @@
{
"owner": "Administrator",
"docstatus": 0,
- "creation": "2012-10-10 10:52:22",
+ "creation": "2012-09-10 12:22:26",
"modified_by": "Administrator",
- "modified": "2012-09-07 11:56:59"
+ "modified": "2012-11-30 12:29:56"
},
{
"is_submittable": 1,
+ "autoname": "naming_series:",
"allow_attach": 1,
"default_print_format": "Standard",
"search_fields": "posting_date, due_date, debit_to, fiscal_year, grand_total, outstanding_amount",
@@ -35,6 +36,15 @@
"doctype": "DocType"
},
{
+ "print_hide": 1,
+ "oldfieldtype": "Section Break",
+ "doctype": "DocField",
+ "label": "Basic Info",
+ "fieldname": "basic_info",
+ "fieldtype": "Section Break",
+ "permlevel": 0
+ },
+ {
"print_hide": 0,
"oldfieldtype": "Column Break",
"doctype": "DocField",
@@ -217,9 +227,9 @@
"permlevel": 0
},
{
- "allow_on_submit": 1,
"oldfieldtype": "Table",
"colour": "White:FFF",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "Entries",
"oldfieldname": "entries",
@@ -428,9 +438,9 @@
"permlevel": 0
},
{
- "allow_on_submit": 1,
"oldfieldtype": "Table",
"colour": "White:FFF",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "Taxes and Charges1",
"oldfieldname": "other_charges",
@@ -1174,6 +1184,7 @@
},
{
"print_hide": 1,
+ "depends_on": "eval:doc.docstatus<2",
"colour": "White:FFF",
"doctype": "DocField",
"label": "Recurring Invoice",
@@ -1193,7 +1204,7 @@
"print_hide": 1,
"description": "Check if recurring invoice, uncheck to stop recurring or put proper End Date",
"no_copy": 1,
- "depends_on": "eval:doc.docstatus==1",
+ "depends_on": "eval:doc.docstatus<2",
"colour": "White:FFF",
"allow_on_submit": 1,
"doctype": "DocField",
@@ -1205,23 +1216,24 @@
},
{
"print_hide": 1,
- "allow_on_submit": 1,
"description": "Select the period when the invoice will be generated automatically",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
+ "colour": "White:FFF",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "Recurring Type",
- "options": "Monthly\nQuarterly\nHalf-yearly\nYearly",
+ "permlevel": 0,
"fieldname": "recurring_type",
"fieldtype": "Select",
- "permlevel": 0
+ "options": "Monthly\nQuarterly\nHalf-yearly\nYearly"
},
{
"print_hide": 1,
- "allow_on_submit": 1,
"description": "The day of the month on which auto invoice will be generated e.g. 05, 28 etc ",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "Repeat on Day of Month",
"fieldname": "repeat_on_day_of_month",
@@ -1230,11 +1242,11 @@
},
{
"print_hide": 1,
- "allow_on_submit": 1,
"description": "Start date of the invoice period",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
"colour": "White:FFF",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "Invoice Period From Date",
"fieldname": "invoice_period_from_date",
@@ -1243,10 +1255,10 @@
},
{
"print_hide": 1,
- "allow_on_submit": 1,
"description": "End date of the invoice period",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "Invoice Period To Date",
"fieldname": "invoice_period_to_date",
@@ -1264,10 +1276,10 @@
},
{
"print_hide": 1,
- "allow_on_submit": 1,
"description": "Enter email id separated by commas, invoice will be mailed automatically on particular date",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "Notification Email Address",
"fieldname": "notification_email_address",
@@ -1276,9 +1288,10 @@
},
{
"print_hide": 1,
- "description": "The unique id for tracking all recurring invoices ",
+ "description": "The unique id for tracking all recurring invoices.\u00a0It is generated on submit.",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
+ "colour": "White:FFF",
"doctype": "DocField",
"label": "Recurring Id",
"fieldname": "recurring_id",
@@ -1287,9 +1300,10 @@
},
{
"print_hide": 1,
- "description": "The date on which next invoice will be generated ",
+ "description": "The date on which next invoice will be generated. It is generated on submit.\n",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
+ "colour": "White:FFF",
"doctype": "DocField",
"label": "Next Date",
"fieldname": "next_date",
@@ -1298,10 +1312,10 @@
},
{
"print_hide": 1,
- "allow_on_submit": 1,
"description": "The date on which recurring invoice will be stop",
"no_copy": 1,
"depends_on": "eval:doc.convert_into_recurring_invoice==1",
+ "allow_on_submit": 1,
"doctype": "DocField",
"label": "End Date",
"fieldname": "end_date",
diff --git a/accounts/page/accounts_browser/accounts_browser.js b/accounts/page/accounts_browser/accounts_browser.js
index 3ef5045..323135b 100644
--- a/accounts/page/accounts_browser/accounts_browser.js
+++ b/accounts/page/accounts_browser/accounts_browser.js
@@ -185,9 +185,9 @@
// tax rate if tax
$(fd.account_type.input).change(function() {
if($(this).val()=='Tax') {
- $(fd.tax_rate.wrapper).toggle(true);
+ $(fd.tax_rate.wrapper).toggle(true);
} else {
- $(fd.tax_rate.wrapper).toggle(false);
+ $(fd.tax_rate.wrapper).toggle(false);
}
})
@@ -214,6 +214,7 @@
// show
d.onshow = function() {
$(fd.group_or_ledger.input).change();
+ $(fd.account_type.input).change();
}
d.show();
},
diff --git a/controllers/tax_controller.py b/controllers/tax_controller.py
new file mode 100644
index 0000000..6df17f5
--- /dev/null
+++ b/controllers/tax_controller.py
@@ -0,0 +1,433 @@
+# ERPNext - web based ERP (http://erpnext.com)
+# Copyright (C) 2012 Web Notes Technologies Pvt Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+import webnotes
+import webnotes.model
+from webnotes import _, msgprint
+from webnotes.utils import cint, flt
+from webnotes.model.utils import round_doc
+import json
+
+from controllers.transaction_controller import TransactionController
+
+class TaxController(TransactionController):
+ def append_taxes(self):
+ """append taxes as per tax master link field"""
+ # clear tax table
+ self.doclist = self.doclist.get({"parentfield": ["!=",
+ self.fmap.taxes_and_charges]})
+
+ tax_master_doctype = self.meta.get_options(self.fmap.taxes_and_charges_master)
+ master_tax_list = webnotes.get_doclist(tax_master_doctype,
+ self.doc.fields.get(self.fmap.taxes_and_charges_master)).get(
+ {"parentfield": self.fmap.taxes_and_charges})
+
+ for base_tax in master_tax_list:
+ tax = DictObj([[field, base_tax.fields.get(field)]
+ for field in base_tax.fields
+ if field not in webnotes.model.default_fields])
+ tax.update({
+ "doctype": self.meta.get_options(self.fmap.taxes_and_charges),
+ "parentfield": self.fmap.taxes_and_charges,
+ "rate": flt(tax.rate, self.precision.tax.rate),
+ })
+ self.doclist.append(tax)
+
+ def calculate_taxes_and_totals(self):
+ """
+ Calculates:
+ * amount for each item
+ * valuation_tax_amount for each item,
+ * tax amount and tax total for each tax
+ * net total
+ * total taxes
+ * grand total
+ """
+ self.doc.fields[self.fmap.exchange_rate] = \
+ flt(self.doc.fields.get(self.fmap.exchange_rate),
+ self.precision.main[self.fmap.exchange_rate])
+
+ self.calculate_item_values()
+
+ self.initialize_taxes()
+ if self.meta.get_field("included_in_print_rate",
+ parentfield=self.fmap.taxes_and_charges):
+ self.determine_exclusive_rate()
+
+ self.calculate_net_total()
+ self.calculate_taxes()
+ self.calculate_totals()
+ self.set_amount_in_words()
+
+ def calculate_item_values(self):
+ def _set_base(item, print_field, base_field):
+ """set values in base currency"""
+ item.fields[base_field] = flt((flt(item.fields[print_field],
+ self.precision.item[print_field]) * \
+ self.doc.fields.get(self.fmap.exchange_rate)),
+ self.precision.item[base_field])
+
+ for item in self.item_doclist:
+ round_doc(item, self.precision.item)
+
+ if item.fields.get(self.fmap.discount) == 100:
+ if not item.fields.get(self.fmap.print_ref_rate):
+ item.fields[self.fmap.print_ref_rate] = \
+ item.fields.get(self.fmap.print_rate)
+ item.fields[self.fmap.print_rate] = 0
+ else:
+ if item.fields.get(self.fmap.print_ref_rate):
+ item.fields[self.fmap.print_rate] = \
+ flt(item.fields.get(self.fmap.print_ref_rate) *
+ (1.0 - (item.fields.get(self.fmap.discount) / 100.0)),
+ self.precision.item[self.fmap.print_rate])
+ else:
+ # assume that print rate and discount are specified
+ item.fields[self.fmap.print_ref_rate] = \
+ flt(item.fields.get(self.fmap.print_rate) /
+ (1.0 - (item.fields.get(self.fmap.discount) / 100.0)),
+ self.precision.item[self.fmap.print_ref_rate])
+
+ item.fields[self.fmap.print_amount] = \
+ flt(item.fields.get(self.fmap.print_rate) * \
+ item.fields.get(self.fmap.qty),
+ self.precision.item[self.fmap.print_amount])
+
+ _set_base(item, self.fmap.print_ref_rate, self.fmap.ref_rate)
+ _set_base(item, self.fmap.print_rate, self.fmap.rate)
+ _set_base(item, self.fmap.print_amount, self.fmap.amount)
+
+ def initialize_taxes(self):
+ for tax in self.tax_doclist:
+ # initialize totals to 0
+ tax.tax_amount = tax.total = tax.total_print = 0
+ tax.grand_total_for_current_item = tax.tax_amount_for_current_item = 0
+
+ # for actual type, user can mention actual tax amount in tax.tax_amount_print
+ if tax.charge_type != "Actual" or tax.rate:
+ tax.tax_amount_print = 0
+
+ self.validate_on_previous_row(tax)
+ self.validate_included_tax(tax)
+
+ # round relevant values
+ round_doc(tax, self.precision.tax)
+
+ def calculate_net_total(self):
+ self.doc.net_total = 0
+ self.doc.fields[self.fmap.net_total_print] = 0
+
+ for item in self.item_doclist:
+ self.doc.net_total += item.amount
+ self.doc.fields[self.fmap.net_total_print] += \
+ item.fields.get(self.fmap.print_amount)
+
+ self.doc.net_total = flt(self.doc.net_total, self.precision.main.net_total)
+ self.doc.fields[self.fmap.net_total_print] = \
+ flt(self.doc.fields.get(self.fmap.net_total_print),
+ self.precision.main[self.fmap.net_total_print])
+
+ def calculate_taxes(self):
+ for item in self.item_doclist:
+ item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
+ item.fields[self.fmap.valuation_tax_amount] = 0
+
+ for i, tax in enumerate(self.tax_doclist):
+ # tax_amount represents the amount of tax for the current step
+ current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
+
+ if hasattr(self, "set_valuation_tax_amount"):
+ self.set_valuation_tax_amount(item, tax, current_tax_amount)
+
+ # case when net total is 0 but there is an actual type charge
+ # in this case add the actual amount to tax.tax_amount
+ # and tax.grand_total_for_current_item for the first such iteration
+ if not (current_tax_amount or self.doc.net_total or tax.tax_amount) and \
+ tax.charge_type=="Actual":
+ zero_net_total_adjustment = flt((tax.tax_amount_print *
+ self.doc.fields.get(self.fmap.exchange_rate)) or tax.rate,
+ self.precision.tax.tax_amount)
+ current_tax_amount += zero_net_total_adjustment
+
+ # store tax_amount for current item as it will be used for
+ # charge type = 'On Previous Row Amount'
+ tax.tax_amount_for_current_item = current_tax_amount
+
+ # accumulate tax amount into tax.tax_amount
+ tax.tax_amount += tax.tax_amount_for_current_item
+
+ # accumulate tax_amount_print only if tax is not included
+ # and if tax amount of actual type is entered in 'rate' field
+ if not cint(tax.included_in_print_rate) and (tax.charge_type != "Actual"
+ or tax.rate):
+ tax.tax_amount_print += flt((tax.tax_amount_for_current_item /
+ self.doc.fields.get(self.fmap.exchange_rate)),
+ self.precision.tax.tax_amount_print)
+
+ if tax.category == "Valuation":
+ # if just for valuation, do not add the tax amount in total
+ # hence, setting it as 0 for further steps
+ current_tax_amount = 0
+
+ # Calculate tax.total viz. grand total till that step
+ # note: grand_total_for_current_item contains the contribution of
+ # item's amount, previously applied tax and the current tax on that item
+ if i==0:
+ tax.grand_total_for_current_item = flt(item.amount +
+ current_tax_amount, self.precision.tax.total)
+
+ # if inclusive pricing, current_tax_amount should not be considered
+ if cint(tax.included_in_print_rate):
+ current_tax_amount = 0
+
+ tax.grand_total_print_for_current_item = \
+ flt(item.fields.get(self.fmap.print_amount) +
+ (current_tax_amount / self.doc.fields.get(
+ self.fmap.exchange_rate)),
+ self.precision.tax.total_print)
+ else:
+ tax.grand_total_for_current_item = \
+ flt(self.tax_doclist[i-1].grand_total_for_current_item +
+ current_tax_amount, self.precision.tax.total)
+
+ # if inclusive pricing, current_tax_amount should not be considered
+ if cint(tax.included_in_print_rate):
+ current_tax_amount = 0
+
+ tax.grand_total_print_for_current_item = \
+ flt(self.tax_doclist[i-1].grand_total_print_for_current_item +
+ (current_tax_amount / self.doc.fields.get(
+ self.fmap.exchange_rate)),
+ self.precision.tax.total_print)
+
+ # in tax.total, accumulate grand total of each item
+ tax.total += tax.grand_total_for_current_item
+ tax.total_print += tax.grand_total_print_for_current_item
+
+ # TODO store tax_breakup for each item
+
+ def get_current_tax_amount(self, item, tax, item_tax_map):
+ tax_rate = self._get_tax_rate(tax, item_tax_map)
+
+ if tax.charge_type == "Actual":
+ # distribute the tax amount proportionally to each item row
+ actual = flt(tax.rate or (tax.tax_amount_print * \
+ self.doc.fields.get(self.fmap.exchange_rate)),
+ self.precision.tax.tax_amount)
+ current_tax_amount = (self.doc.net_total
+ and ((item.amount / self.doc.net_total) * actual)
+ or 0)
+ elif tax.charge_type == "On Net Total":
+ current_tax_amount = (tax_rate / 100.0) * item.amount
+ elif tax.charge_type == "On Previous Row Amount":
+ current_tax_amount = (tax_rate / 100.0) * \
+ self.tax_doclist[cint(tax.row_id) - 1].tax_amount_for_current_item
+ elif tax.charge_type == "On Previous Row Total":
+ current_tax_amount = (tax_rate / 100.0) * \
+ self.tax_doclist[cint(tax.row_id) - 1].grand_total_for_current_item
+
+ return flt(current_tax_amount, self.precision.tax.tax_amount)
+
+ def calculate_totals(self):
+ if self.tax_doclist:
+ self.doc.grand_total = flt(self.tax_doclist[-1].total,
+ self.precision.main.grand_total)
+ self.doc.fields[self.fmap.grand_total_print] = \
+ flt(self.tax_doclist[-1].total_print,
+ self.precision.main[self.fmap.grand_total_print])
+ else:
+ self.doc.grand_total = flt(self.doc.net_total,
+ self.precision.main.grand_total)
+ self.doc.fields[self.fmap.grand_total_print] = \
+ flt(self.doc.fields.get(self.fmap.net_total_print),
+ self.precision.main[self.fmap.grand_total_print])
+
+ self.doc.fields[self.fmap.taxes_and_charges_total] = \
+ flt(self.doc.grand_total - self.doc.net_total,
+ self.precision.main[self.fmap.taxes_and_charges_total])
+
+ self.doc.taxes_and_charges_total_print = \
+ flt(self.doc.fields.get(self.fmap.grand_total_print) - \
+ self.doc.fields.get(self.fmap.net_total_print),
+ self.precision.main.taxes_and_charges_total_print)
+
+ self.doc.rounded_total = round(self.doc.grand_total)
+ self.doc.fields[self.fmap.rounded_total_print] = \
+ round(self.doc.fields.get(self.fmap.grand_total_print))
+
+ def set_amount_in_words(self):
+ from webnotes.utils import money_in_words
+ base_currency = webnotes.conn.get_value("Company", self.doc.currency,
+ "default_currency")
+
+ self.doc.fields[self.fmap.grand_total_in_words] = \
+ money_in_words(self.doc.grand_total, base_currency)
+ self.doc.fields[self.fmap.rounded_total_in_words] = \
+ money_in_words(self.doc.rounded_total, base_currency)
+
+ self.doc.fields[self.fmap.grand_total_in_words_print] = \
+ money_in_words(self.doc.fields.get(self.fmap.grand_total_print),
+ self.doc.currency)
+ self.doc.fields[self.fmap.rounded_total_in_words_print] = \
+ money_in_words(self.doc.fields.get(self.fmap.rounded_total_print),
+ self.doc.currency)
+
+ def validate_on_previous_row(self, tax):
+ """
+ validate if a valid row id is mentioned in case of
+ On Previous Row Amount and On Previous Row Total
+ """
+ if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"] and \
+ (not tax.row_id or cint(tax.row_id) >= tax.idx):
+ msgprint((_("Row") + " # %(idx)s [%(taxes_doctype)s]: " + \
+ _("Please specify a valid") + " %(row_id_label)s") % {
+ "idx": tax.idx,
+ "taxes_doctype": tax.parenttype,
+ "row_id_label": self.meta.get_label("row_id",
+ parentfield=self.fmap.taxes_and_charges)
+ }, raise_exception=True)
+
+ def validate_included_tax(self, tax):
+ """
+ validate conditions related to "Is this Tax Included in Rate?"
+ """
+ def _on_previous_row_error(tax, row_range):
+ msgprint((_("Row") + " # %(idx)s [%(taxes_doctype)s]: " + \
+ _("If") + " '%(inclusive_label)s' " + _("is checked for") + \
+ " '%(charge_type_label)s' = '%(charge_type)s', " + _("then") + " " + \
+ _("Row") + " # %(row_range)s " + _("should also have") + \
+ " '%(inclusive_label)s' = " + _("checked")) % {
+ "idx": tax.idx,
+ "taxes_doctype": tax.doctype,
+ "inclusive_label": self.meta.get_label("included_in_print_rate",
+ parentfield=self.fmap.taxes_and_charges),
+ "charge_type_label": self.meta.get_label("charge_type",
+ parentfield=self.fmap.taxes_and_charges),
+ "charge_type": tax.charge_type,
+ "row_range": row_range,
+ }, raise_exception=True)
+
+ if cint(tax.included_in_print_rate):
+ if tax.charge_type == "Actual":
+ # now inclusive rate for type 'Actual'
+ msgprint((_("Row") + " # %(idx)s [%(taxes_doctype)s]: " + \
+ "'%(charge_type_label)s' = '%(charge_type)s' " + \
+ _("cannot be included in item's rate")) % {
+ "idx": tax.idx,
+ "taxes_doctype": self.meta.get_options(
+ self.fmap.taxes_and_charges),
+ "charge_type_label": self.meta.get_label("charge_type",
+ parentfield=self.fmap.taxes_and_charges),
+ "charge_type": tax.charge_type,
+ }, raise_exception=True)
+
+ elif tax.charge_type == "On Previous Row Amount" and \
+ not cint(self.tax_doclist[cint(tax.row_id) - 1]\
+ .included_in_print_rate):
+ # for an inclusive tax of type "On Previous Row Amount",
+ # dependent row should also be inclusive
+ _on_previous_row_error(tax, tax.row_id)
+
+ elif tax.charge_type == "On Previous Row Total" and \
+ not all([cint(t.included_in_print_rate) \
+ for t in self.tax_doclist[:tax.idx - 1]]):
+ # for an inclusive tax of type "On Previous Row Total",
+ # all rows above it should also be inclusive
+ _on_previous_row_error(tax, "1 - %d" % (tax.idx - 1))
+
+ def determine_exclusive_rate(self):
+ if not any((cint(tax.included_in_print_rate) for tax in self.tax_doclist)):
+ # if no tax is marked as included in print rate, no need to proceed further
+ return
+
+ for item in self.item_doclist:
+ item_tax_map = self._load_item_tax_rate(item.item_tax_rate)
+
+ cumulated_tax_fraction = 0
+
+ for i, tax in enumerate(self.tax_doclist):
+ if cint(tax.included_in_print_rate):
+ tax.tax_fraction_for_current_item = \
+ self.get_current_tax_fraction(tax, item_tax_map)
+ else:
+ tax.tax_fraction_for_current_item = 0
+
+ if i==0:
+ tax.grand_total_fraction_for_current_item = 1 + \
+ tax.tax_fraction_for_current_item
+ else:
+ tax.grand_total_fraction_for_current_item = \
+ self.tax_doclist[i-1].grand_total_fraction_for_current_item \
+ + tax.tax_fraction_for_current_item
+
+ cumulated_tax_fraction += tax.tax_fraction_for_current_item
+
+ if cumulated_tax_fraction:
+ item.fields[self.fmap.rate] = \
+ flt((item.fields.get(self.fmap.print_rate) * \
+ self.doc.fields.get(self.fmap.exchange_rate)) /
+ (1 + cumulated_tax_fraction), self.precision.item[self.fmap.rate])
+
+ item.amount = flt(item.fields.get(self.fmap.rate) * item.qty,
+ self.precision.item.amount)
+
+ item.fields[self.fmap.ref_rate] = \
+ flt(item.fields.get(self.fmap.rate) / (1 - \
+ (item.fields.get(self.fmap.discount) / 100.0)),
+ self.precision.item[self.fmap.ref_rate])
+
+ # print item.print_rate, 1+cumulated_tax_fraction, item.rate, item.amount
+ # print "-"*10
+
+ def get_current_tax_fraction(self, tax, item_tax_map):
+ """
+ Get tax fraction for calculating tax exclusive amount
+ from tax inclusive amount
+ """
+ current_tax_fraction = 0
+
+ if cint(tax.included_in_print_rate):
+ tax_rate = self._get_tax_rate(tax, item_tax_map)
+
+ if tax.charge_type == "On Net Total":
+ current_tax_fraction = tax_rate / 100.0
+
+ elif tax.charge_type == "On Previous Row Amount":
+ current_tax_fraction = (tax_rate / 100.0) * \
+ self.tax_doclist[cint(tax.row_id) - 1]\
+ .tax_fraction_for_current_item
+
+ elif tax.charge_type == "On Previous Row Total":
+ current_tax_fraction = (tax_rate / 100.0) * \
+ self.tax_doclist[cint(tax.row_id) - 1]\
+ .grand_total_fraction_for_current_item
+
+ # print tax.account_head, tax_rate, current_tax_fraction
+
+ return current_tax_fraction
+
+ def _load_item_tax_rate(self, item_tax_rate):
+ if not item_tax_rate:
+ return {}
+
+ return json.loads(item_tax_rate)
+
+ def _get_tax_rate(self, tax, item_tax_map):
+ if item_tax_map.has_key(tax.account_head):
+ return flt(item_tax_map.get(tax.account_head), self.precision.tax.rate)
+ else:
+ return tax.rate
diff --git a/controllers/transaction_controller.py b/controllers/transaction_controller.py
new file mode 100644
index 0000000..39a7376
--- /dev/null
+++ b/controllers/transaction_controller.py
@@ -0,0 +1,121 @@
+# ERPNext - web based ERP (http://erpnext.com)
+# Copyright (C) 2012 Web Notes Technologies Pvt Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+import webnotes
+import webnotes.model
+from webnotes import _, msgprint, DictObj
+from webnotes.utils import cint, formatdate, cstr, flt
+from webnotes.model.code import get_obj
+from webnotes.model.doc import make_autoname, Document
+import json
+
+import stock.utils
+
+from webnotes.model.controller import DocListController
+
+class TransactionController(DocListController):
+ def __init__(self, doc, doclist):
+ super(TransactionController, self).__init__(doc, doclist)
+ self.cur_docstatus = cint(webnotes.conn.get_value(self.doc.doctype,
+ self.doc.name, "docstatus"))
+
+ @property
+ def precision(self):
+ if not hasattr(self, "_precision"):
+ self._precision = DictObj()
+ self._precision.main = self.meta.get_precision_map()
+ self._precision.item = self.meta.get_precision_map(parentfield = \
+ self.item_table_field)
+ if self.meta.get_field(self.fmap.taxes_and_charges):
+ self._precision.tax = self.meta.get_precision_map(parentfield = \
+ self.fmap.taxes_and_charges)
+ return self._precision
+
+ @property
+ def item_doclist(self):
+ if not hasattr(self, "_item_doclist"):
+ self._item_doclist = self.doclist.get({"parentfield": self.item_table_field})
+ return self._item_doclist
+
+ @property
+ def tax_doclist(self):
+ if not hasattr(self, "_tax_doclist"):
+ self._tax_doclist = self.doclist.get(
+ {"parentfield": self.fmap.taxes_and_charges})
+ return self._tax_doclist
+
+ @property
+ def stock_items(self):
+ if not hasattr(self, "_stock_items"):
+ item_codes = list(set(item.item_code for item in self.item_doclist))
+ self._stock_items = [r[0] for r in webnotes.conn.sql("""select name
+ from `tabItem` where name in (%s) and is_stock_item='Yes'""" % \
+ (", ".join((["%s"]*len(item_codes))),), item_codes)]
+
+ return self._stock_items
+
+ @property
+ def fmap(self):
+ if not hasattr(self, "_fmap"):
+ if self.doc.doctype in ["Lead", "Quotation", "Sales Order", "Sales Invoice",
+ "Delivery Note"]:
+ self._fmap = webnotes.DictObj( {
+ "exchange_rate": "conversion_rate",
+ "taxes_and_charges": "other_charges",
+ "taxes_and_charges_master": "charge",
+ "taxes_and_charges_total": "other_charges_total",
+ "net_total_print": "net_total_print",
+ "grand_total_print": "grand_total_export",
+ "grand_total_in_words": "grand_total_in_words",
+ "grand_total_in_words_print": "grand_total_in_words_print",
+ "rounded_total_print": "rounded_total_export",
+ "rounded_total_in_words": "in_words",
+ "rounded_total_in_words_print": "in_words_export",
+ "print_ref_rate": "ref_rate",
+ "discount": "adj_rate",
+ "print_rate": "export_rate",
+ "print_amount": "export_amount",
+ "ref_rate": "base_ref_rate",
+ "rate": "basic_rate",
+
+ "plc_exchange_rate": "plc_conversion_rate",
+ "tax_calculation": "other_charges_calculation",
+ })
+ else:
+ self._fmap = webnotes.DictObj({
+ "exchange_rate": "conversion_rate",
+ "taxes_and_charges": "purchase_tax_details",
+ "taxes_and_charges_master": "purchase_other_charges",
+ "taxes_and_charges_total": "total_tax",
+ "net_total_print": "net_total_import",
+ "grand_total_print": "grand_total_import",
+ "grand_total_in_words": "in_words",
+ "grand_total_in_words_print": "in_words_import",
+ "rounded_total_print": "rounded_total_print",
+ "rounded_total_in_words": "rounded_total_in_words",
+ "rounded_total_in_words_print": "rounded_total_in_words_print",
+ "print_ref_rate": "import_ref_rate",
+ "discount": "discount_rate",
+ "print_rate": "import_rate",
+ "print_amount": "import_amount",
+ "ref_rate": "purchase_ref_rate",
+ "rate": "purchase_rate",
+
+ "valuation_tax_amount": "item_tax_amount"
+ })
+
+ return self._fmap or webnotes.DictObj()
\ No newline at end of file
diff --git a/patches/november_2012/production_order_patch.py b/patches/november_2012/production_order_patch.py
index e4423fc..4bc0912 100644
--- a/patches/november_2012/production_order_patch.py
+++ b/patches/november_2012/production_order_patch.py
@@ -1,5 +1,9 @@
def execute():
import webnotes
+
+ webnotes.reload_doc("production", "doctype", "production_order")
+ webnotes.reload_doc("stock", "doctype", "stock_entry")
+
webnotes.conn.sql("""update `tabProduction Order`
set use_multi_level_bom = if(consider_sa_items='Yes', 0, 1)""")
diff --git a/startup/schedule_handlers.py b/startup/schedule_handlers.py
index f3eb05a..668c11d 100644
--- a/startup/schedule_handlers.py
+++ b/startup/schedule_handlers.py
@@ -39,7 +39,7 @@
run_fn(send)
# run recurring invoices
- from accounts.doctype.gl_control.gl_control import manage_recurring_invoices
+ from accounts.doctype.sales_invoice.sales_invoice import manage_recurring_invoices
run_fn(manage_recurring_invoices)
# send bulk emails