diff --git a/controllers/tax_controller.py b/controllers/tax_controller.py new file mode 100644 index 0000000000..6df17f54f5 --- /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 . + +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 0000000000..39a737697c --- /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 . + +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