From 21f4ea36b21c40c97e5b10789514b0969bce68e5 Mon Sep 17 00:00:00 2001 From: Anand Doshi Date: Fri, 10 May 2013 18:08:32 +0530 Subject: [PATCH] [selling] [calculations] server side calculations, test cases and rounding based on currency number format --- .../doctype/sales_invoice/sales_invoice.txt | 22 +- .../sales_invoice/test_sales_invoice.py | 477 +++++++++++++++++- controllers/accounts_controller.py | 2 +- controllers/buying_controller.py | 64 +-- controllers/selling_controller.py | 281 ++++++++++- patches/may_2013/__init__.py | 0 .../may_2013/p01_selling_net_total_export.py | 10 + patches/patch_list.py | 1 + selling/doctype/quotation/quotation.txt | 14 +- selling/doctype/sales_common/sales_common.js | 2 + selling/doctype/sales_order/sales_order.txt | 19 +- stock/doctype/delivery_note/delivery_note.txt | 19 +- stock/doctype/stock_entry/test_stock_entry.py | 1 + 13 files changed, 843 insertions(+), 69 deletions(-) create mode 100644 patches/may_2013/__init__.py create mode 100644 patches/may_2013/p01_selling_net_total_export.py diff --git a/accounts/doctype/sales_invoice/sales_invoice.txt b/accounts/doctype/sales_invoice/sales_invoice.txt index 9d8f54e73f..f1c0cab74d 100644 --- a/accounts/doctype/sales_invoice/sales_invoice.txt +++ b/accounts/doctype/sales_invoice/sales_invoice.txt @@ -1,8 +1,8 @@ [ { - "creation": "2013-04-19 11:00:14", + "creation": "2013-05-06 12:03:41", "docstatus": 0, - "modified": "2013-04-22 11:59:40", + "modified": "2013-05-09 17:34:14", "modified_by": "Administrator", "owner": "Administrator" }, @@ -10,6 +10,7 @@ "allow_attach": 1, "autoname": "naming_series:", "doctype": "DocType", + "document_type": "Transaction", "is_submittable": 1, "module": "Accounts", "name": "__common__", @@ -30,6 +31,7 @@ "parent": "Sales Invoice", "parentfield": "permissions", "parenttype": "DocType", + "permlevel": 0, "read": 1 }, { @@ -251,7 +253,6 @@ "width": "50%" }, { - "description": "Will be calculated automatically when you enter the details", "doctype": "DocField", "fieldname": "net_total", "fieldtype": "Currency", @@ -259,10 +260,19 @@ "oldfieldname": "net_total", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "print_hide": 0, + "print_hide": 1, "read_only": 1, "reqd": 1 }, + { + "doctype": "DocField", + "fieldname": "net_total_export", + "fieldtype": "Currency", + "label": "Net Total (Export)", + "options": "currency", + "print_hide": 0, + "read_only": 1 + }, { "doctype": "DocField", "fieldname": "recalculate_values", @@ -1288,7 +1298,6 @@ "cancel": 1, "create": 1, "doctype": "DocPerm", - "permlevel": 0, "report": 1, "role": "Accounts User", "submit": 1, @@ -1297,8 +1306,7 @@ { "doctype": "DocPerm", "match": "customer", - "permlevel": 0, - "report": 1, + "report": 0, "role": "Customer" } ] \ No newline at end of file diff --git a/accounts/doctype/sales_invoice/test_sales_invoice.py b/accounts/doctype/sales_invoice/test_sales_invoice.py index b46cdd1777..505848a3c8 100644 --- a/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1,12 +1,226 @@ import webnotes -import unittest +import unittest, json +from webnotes.utils import flt class TestSalesInvoice(unittest.TestCase): def make(self): - w = webnotes.bean(webnotes.copy_doclist(test_records[0])) + w = webnotes.bean(copy=test_records[0]) w.insert() w.submit() return w + + def test_sales_invoice_calculation_base_currency(self): + si = webnotes.bean(copy=test_records[2]) + si.run_method("calculate_taxes_and_totals") + si.insert() + + expected_values = { + "keys": ["ref_rate", "adj_rate", "export_rate", "export_amount", + "base_ref_rate", "basic_rate", "amount"], + "_Test Item Home Desktop 100": [50, 0, 50, 500, 50, 50, 500], + "_Test Item Home Desktop 200": [150, 0, 150, 750, 150, 150, 750], + } + + # check if children are saved + self.assertEquals(len(si.doclist.get({"parentfield": "entries"})), + len(expected_values)-1) + + # check if item values are calculated + for d in si.doclist.get({"parentfield": "entries"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(d.fields.get(k), expected_values[d.item_code][i]) + + # check net total + self.assertEquals(si.doc.net_total, 1250) + self.assertEquals(si.doc.net_total_export, 1250) + + # check tax calculation + expected_values = { + "keys": ["tax_amount", "total"], + "_Test Account Shipping Charges - _TC": [100, 1350], + "_Test Account Customs Duty - _TC": [125, 1475], + "_Test Account Excise Duty - _TC": [140, 1615], + "_Test Account Education Cess - _TC": [2.8, 1617.8], + "_Test Account S&H Education Cess - _TC": [1.4, 1619.2], + "_Test Account CST - _TC": [32.38, 1651.58], + "_Test Account VAT - _TC": [156.25, 1807.83], + "_Test Account Discount - _TC": [-180.78, 1627.05] + } + + for d in si.doclist.get({"parentfield": "other_charges"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(d.fields.get(k), expected_values[d.account_head][i]) + + self.assertEquals(si.doc.grand_total, 1627.05) + self.assertEquals(si.doc.grand_total_export, 1627.05) + + def test_sales_invoice_calculation_export_currency(self): + si = webnotes.bean(copy=test_records[2]) + si.doc.currency = "USD" + si.doc.conversion_rate = 50 + si.doclist[1].export_rate = 1 + si.doclist[1].ref_rate = 1 + si.doclist[2].export_rate = 3 + si.doclist[2].ref_rate = 3 + si.run_method("calculate_taxes_and_totals") + si.insert() + + expected_values = { + "keys": ["ref_rate", "adj_rate", "export_rate", "export_amount", + "base_ref_rate", "basic_rate", "amount"], + "_Test Item Home Desktop 100": [1, 0, 1, 10, 50, 50, 500], + "_Test Item Home Desktop 200": [3, 0, 3, 15, 150, 150, 750], + } + + # check if children are saved + self.assertEquals(len(si.doclist.get({"parentfield": "entries"})), + len(expected_values)-1) + + # check if item values are calculated + for d in si.doclist.get({"parentfield": "entries"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(d.fields.get(k), expected_values[d.item_code][i]) + + # check net total + self.assertEquals(si.doc.net_total, 1250) + self.assertEquals(si.doc.net_total_export, 25) + + # check tax calculation + expected_values = { + "keys": ["tax_amount", "total"], + "_Test Account Shipping Charges - _TC": [100, 1350], + "_Test Account Customs Duty - _TC": [125, 1475], + "_Test Account Excise Duty - _TC": [140, 1615], + "_Test Account Education Cess - _TC": [2.8, 1617.8], + "_Test Account S&H Education Cess - _TC": [1.4, 1619.2], + "_Test Account CST - _TC": [32.38, 1651.58], + "_Test Account VAT - _TC": [156.25, 1807.83], + "_Test Account Discount - _TC": [-180.78, 1627.05] + } + + for d in si.doclist.get({"parentfield": "other_charges"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(d.fields.get(k), expected_values[d.account_head][i]) + + self.assertEquals(si.doc.grand_total, 1627.05) + self.assertEquals(si.doc.grand_total_export, 32.54) + + def test_inclusive_rate_validations(self): + si = webnotes.bean(copy=test_records[2]) + for i, tax in enumerate(si.doclist.get({"parentfield": "other_charges"})): + tax.idx = i+1 + + si.doclist[1].export_rate = 62.5 + si.doclist[1].export_rate = 191 + for i in [3, 5, 6, 7, 8, 9]: + si.doclist[i].included_in_print_rate = 1 + + # tax type "Actual" cannot be inclusive + self.assertRaises(webnotes.ValidationError, si.run_method, "calculate_taxes_and_totals") + + # taxes above included type 'On Previous Row Total' should also be included + si.doclist[3].included_in_print_rate = 0 + self.assertRaises(webnotes.ValidationError, si.run_method, "calculate_taxes_and_totals") + + def test_sales_invoice_calculation_base_currency_with_tax_inclusive_price(self): + # prepare + si = webnotes.bean(copy=test_records[3]) + si.run_method("calculate_taxes_and_totals") + si.insert() + + expected_values = { + "keys": ["ref_rate", "adj_rate", "export_rate", "export_amount", + "base_ref_rate", "basic_rate", "amount"], + "_Test Item Home Desktop 100": [62.5, 0, 62.5, 625.0, 50, 50, 500], + "_Test Item Home Desktop 200": [190.66, 0, 190.66, 953.3, 150, 150, 750], + } + + # check if children are saved + self.assertEquals(len(si.doclist.get({"parentfield": "entries"})), + len(expected_values)-1) + + # check if item values are calculated + for d in si.doclist.get({"parentfield": "entries"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(d.fields.get(k), expected_values[d.item_code][i]) + + # check net total + self.assertEquals(si.doc.net_total, 1250) + self.assertEquals(si.doc.net_total_export, 1578.3) + + # check tax calculation + expected_values = { + "keys": ["tax_amount", "total"], + "_Test Account Excise Duty - _TC": [140, 1390], + "_Test Account Education Cess - _TC": [2.8, 1392.8], + "_Test Account S&H Education Cess - _TC": [1.4, 1394.2], + "_Test Account CST - _TC": [27.88, 1422.08], + "_Test Account VAT - _TC": [156.25, 1578.33], + "_Test Account Customs Duty - _TC": [125, 1703.33], + "_Test Account Shipping Charges - _TC": [100, 1803.33], + "_Test Account Discount - _TC": [-180.33, 1623] + } + + for d in si.doclist.get({"parentfield": "other_charges"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(flt(d.fields.get(k), 6), expected_values[d.account_head][i]) + + self.assertEquals(si.doc.grand_total, 1623) + self.assertEquals(si.doc.grand_total_export, 1623) + + def test_sales_invoice_calculation_export_currency_with_tax_inclusive_price(self): + # prepare + si = webnotes.bean(copy=test_records[3]) + si.doc.currency = "USD" + si.doc.conversion_rate = 50 + si.doclist[1].export_rate = 50 + si.doclist[1].adj_rate = 10 + si.doclist[2].export_rate = 150 + si.doclist[2].adj_rate = 20 + si.doclist[9].rate = 5000 + + si.run_method("calculate_taxes_and_totals") + si.insert() + + expected_values = { + "keys": ["ref_rate", "adj_rate", "export_rate", "export_amount", + "base_ref_rate", "basic_rate", "amount"], + "_Test Item Home Desktop 100": [55.56, 10, 50, 500, 2222.11, 1999.9, 19999.0], + "_Test Item Home Desktop 200": [187.5, 20, 150, 750, 7375.66, 5900.53, 29502.65], + } + + # check if children are saved + self.assertEquals(len(si.doclist.get({"parentfield": "entries"})), + len(expected_values)-1) + + # check if item values are calculated + for d in si.doclist.get({"parentfield": "entries"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(d.fields.get(k), expected_values[d.item_code][i]) + + # check net total + self.assertEquals(si.doc.net_total, 49501.65) + self.assertEquals(si.doc.net_total_export, 1250) + + # check tax calculation + expected_values = { + "keys": ["tax_amount", "total"], + "_Test Account Excise Duty - _TC": [5540.22, 55041.87], + "_Test Account Education Cess - _TC": [110.81, 55152.68], + "_Test Account S&H Education Cess - _TC": [55.4, 55208.08], + "_Test Account CST - _TC": [1104.16, 56312.24], + "_Test Account VAT - _TC": [6187.71, 62499.95], + "_Test Account Customs Duty - _TC": [4950.17, 67450.12], + "_Test Account Shipping Charges - _TC": [5000, 72450.12], + "_Test Account Discount - _TC": [-7245.01, 65205.11] + } + + for d in si.doclist.get({"parentfield": "other_charges"}): + for i, k in enumerate(expected_values["keys"]): + self.assertEquals(flt(d.fields.get(k), 6), expected_values[d.account_head][i]) + + self.assertEquals(si.doc.grand_total, 65205.11) + self.assertEquals(si.doc.grand_total_export, 1304.1) def test_outstanding(self): w = self.make() @@ -520,4 +734,263 @@ test_records = [ "tax_amount": 50.0, } ], + [ + { + "naming_series": "_T-Sales Invoice-", + "company": "_Test Company", + "conversion_rate": 1.0, + "currency": "INR", + "debit_to": "_Test Customer - _TC", + "customer": "_Test Customer", + "customer_name": "_Test Customer", + "doctype": "Sales Invoice", + "due_date": "2013-01-23", + "fiscal_year": "_Test Fiscal Year 2013", + "grand_total_export": 0, + "plc_conversion_rate": 1.0, + "posting_date": "2013-01-23", + "price_list_currency": "INR", + "price_list_name": "_Test Price List", + "territory": "_Test Territory", + }, + # items + { + "doctype": "Sales Invoice Item", + "parentfield": "entries", + "item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 10, + "export_rate": 50, + "stock_uom": "_Test UOM", + "item_tax_rate": json.dumps({"_Test Account Excise Duty - _TC": 10}), + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + + }, + { + "doctype": "Sales Invoice Item", + "parentfield": "entries", + "item_code": "_Test Item Home Desktop 200", + "item_name": "_Test Item Home Desktop 200", + "qty": 5, + "export_rate": 150, + "stock_uom": "_Test UOM", + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + + }, + # taxes + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "Actual", + "account_head": "_Test Account Shipping Charges - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Shipping Charges", + "rate": 100 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Net Total", + "account_head": "_Test Account Customs Duty - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Customs Duty", + "rate": 10 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Net Total", + "account_head": "_Test Account Excise Duty - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "rate": 12 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "rate": 2, + "row_id": 3 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account S&H Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "rate": 1, + "row_id": 3 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Total", + "account_head": "_Test Account CST - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "CST", + "rate": 2, + "row_id": 5 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "rate": 12.5 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Total", + "account_head": "_Test Account Discount - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Discount", + "rate": -10, + "row_id": 7 + }, + ], + [ + { + "naming_series": "_T-Sales Invoice-", + "company": "_Test Company", + "conversion_rate": 1.0, + "currency": "INR", + "debit_to": "_Test Customer - _TC", + "customer": "_Test Customer", + "customer_name": "_Test Customer", + "doctype": "Sales Invoice", + "due_date": "2013-01-23", + "fiscal_year": "_Test Fiscal Year 2013", + "grand_total_export": 0, + "plc_conversion_rate": 1.0, + "posting_date": "2013-01-23", + "price_list_currency": "INR", + "price_list_name": "_Test Price List", + "territory": "_Test Territory", + }, + # items + { + "doctype": "Sales Invoice Item", + "parentfield": "entries", + "item_code": "_Test Item Home Desktop 100", + "item_name": "_Test Item Home Desktop 100", + "qty": 10, + "export_rate": 62.5, + "stock_uom": "_Test UOM", + "item_tax_rate": json.dumps({"_Test Account Excise Duty - _TC": 10}), + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + + }, + { + "doctype": "Sales Invoice Item", + "parentfield": "entries", + "item_code": "_Test Item Home Desktop 200", + "item_name": "_Test Item Home Desktop 200", + "qty": 5, + "export_rate": 190.66, + "stock_uom": "_Test UOM", + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + + }, + # taxes + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Net Total", + "account_head": "_Test Account Excise Duty - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "rate": 12, + "included_in_print_rate": 1, + "idx": 1 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "rate": 2, + "row_id": 1, + "included_in_print_rate": 1, + "idx": 2 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account S&H Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "rate": 1, + "row_id": 1, + "included_in_print_rate": 1, + "idx": 3 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Total", + "account_head": "_Test Account CST - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "CST", + "rate": 2, + "row_id": 3, + "included_in_print_rate": 1, + "idx": 4 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "rate": 12.5, + "included_in_print_rate": 1, + "idx": 5 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Net Total", + "account_head": "_Test Account Customs Duty - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Customs Duty", + "rate": 10, + "idx": 6 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "Actual", + "account_head": "_Test Account Shipping Charges - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Shipping Charges", + "rate": 100, + "idx": 7 + }, + { + "doctype": "Sales Taxes and Charges", + "parentfield": "other_charges", + "charge_type": "On Previous Row Total", + "account_head": "_Test Account Discount - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Discount", + "rate": -10, + "row_id": 7, + "idx": 8 + }, + ], ] \ No newline at end of file diff --git a/controllers/accounts_controller.py b/controllers/accounts_controller.py index 04e4bbdaee..19b2a50ff9 100644 --- a/controllers/accounts_controller.py +++ b/controllers/accounts_controller.py @@ -97,4 +97,4 @@ class AccountsController(TransactionBase): if not hasattr(self, "_abbr"): self._abbr = webnotes.conn.get_value("Company", self.doc.company, "abbr") - return self._abbr \ No newline at end of file + return self._abbr diff --git a/controllers/buying_controller.py b/controllers/buying_controller.py index 480214018c..d4aa2252eb 100644 --- a/controllers/buying_controller.py +++ b/controllers/buying_controller.py @@ -22,7 +22,6 @@ import json from buying.utils import get_item_details from setup.utils import get_company_currency -from webnotes.model.utils import round_floats_in_doc from controllers.stock_controller import StockController @@ -138,19 +137,20 @@ class BuyingController(StockController): 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.conversion_rate), - self.precision.item[base_field]) + self.precision_of(print_field, item.parentfield)) * self.doc.conversion_rate), + self.precision_of(base_field, item.parentfield)) # hack! - cleaned up in _cleanup() if self.doc.doctype != "Purchase Invoice": - self.precision.item["rate"] = self.precision.item.purchase_rate + df = self.meta.get_field("purchase_rate", parentfield=self.fname) + df.fieldname = "rate" for item in self.item_doclist: # hack! - cleaned up in _cleanup() if self.doc.doctype != "Purchase Invoice": item.rate = item.purchase_rate - round_floats_in_doc(item, self.precision.item) + self.round_floats_in_doc(item, item.parentfield) if item.discount_rate == 100: item.import_ref_rate = item.import_ref_rate or item.import_rate @@ -158,14 +158,14 @@ class BuyingController(StockController): else: if item.import_ref_rate: item.import_rate = flt(item.import_ref_rate * (1.0 - (item.discount_rate / 100.0)), - self.precision.item.import_rate) + self.precision_of("import_rate", item.parentfield)) else: # assume that print rate and discount_rate are specified item.import_ref_rate = flt(item.import_rate / (1.0 - (item.discount_rate / 100.0)), - self.precision.item.import_ref_rate) + self.precision_of("import_ref_rate", item.parentfield)) item.import_amount = flt(item.import_rate * item.qty, - self.precision.item.import_amount) + self.precision_of("import_amount", item.parentfield)) _set_base(item, "import_ref_rate", "purchase_ref_rate") _set_base(item, "import_rate", "rate") @@ -183,7 +183,7 @@ class BuyingController(StockController): self.validate_on_previous_row(tax) - round_floats_in_doc(tax, self.precision.tax) + self.round_floats_in_doc(tax, tax.parentfield) def calculate_net_total(self): self.doc.net_total = 0 @@ -193,9 +193,9 @@ class BuyingController(StockController): self.doc.net_total += item.amount self.doc.net_total_import += item.import_amount - self.doc.net_total = flt(self.doc.net_total, self.precision.main.net_total) + self.doc.net_total = flt(self.doc.net_total, self.precision_of("net_total")) self.doc.net_total_import = flt(self.doc.net_total_import, - self.precision.main.net_total_import) + self.precision_of("net_total_import")) def calculate_taxes(self): for item in self.item_doclist: @@ -213,7 +213,7 @@ class BuyingController(StockController): # 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.rate, self.precision.tax.tax_amount) + zero_net_total_adjustment = flt(tax.rate, self.precision_of("tax_amount", tax.parentfield)) current_tax_amount += zero_net_total_adjustment # store tax_amount for current item as it will be used for @@ -235,12 +235,12 @@ class BuyingController(StockController): # 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) + current_tax_amount, self.precision_of("total", tax.parentfield)) 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) + current_tax_amount, self.precision_of("total", tax.parentfield)) # in tax.total, accumulate grand total of each item tax.total += tax.grand_total_for_current_item @@ -252,20 +252,20 @@ class BuyingController(StockController): def calculate_totals(self): if self.tax_doclist: self.doc.grand_total = flt(self.tax_doclist[-1].total, - self.precision.main.grand_total) + self.precision_of("grand_total")) self.doc.grand_total_import = flt( self.doc.grand_total / self.doc.conversion_rate, - self.precision.main.grand_total_import) + self.precision_of("grand_total_import")) else: self.doc.grand_total = flt(self.doc.net_total, - self.precision.main.grand_total) + self.precision_of("grand_total")) self.doc.grand_total_import = flt( self.doc.grand_total / self.doc.conversion_rate, - self.precision.main.grand_total_import) + self.precision_of("grand_total_import")) self.doc.total_tax = \ flt(self.doc.grand_total - self.doc.net_total, - self.precision.main.total_tax) + self.precision_of("total_tax")) if self.meta.get_field("rounded_total"): self.doc.rounded_total = round(self.doc.grand_total) @@ -276,11 +276,11 @@ class BuyingController(StockController): def calculate_outstanding_amount(self): if self.doc.doctype == "Purchase Invoice" and self.doc.docstatus == 0: self.doc.total_advance = flt(self.doc.total_advance, - self.precision.main.total_advance) + self.precision_of("total_advance")) self.doc.total_amount_to_pay = flt(self.doc.grand_total - flt(self.doc.write_off_amount, - self.precision.main.write_off_amount), self.precision.main.total_amount_to_pay) + self.precision_of("write_off_amount")), self.precision_of("total_amount_to_pay")) self.doc.outstanding_amount = flt(self.doc.total_amount_to_pay - self.doc.total_advance, - self.precision.main.outstanding_amount) + self.precision_of("outstanding_amount")) def _cleanup(self): for tax in self.tax_doclist: @@ -319,7 +319,7 @@ class BuyingController(StockController): if tax.charge_type == "Actual": # distribute the tax amount proportionally to each item row - actual = flt(tax.rate, self.precision.tax.tax_amount) + actual = flt(tax.rate, self.precision_of("tax_amount", tax.parentfield)) current_tax_amount = (self.doc.net_total and ((item.amount / self.doc.net_total) * actual) or 0) @@ -332,11 +332,11 @@ class BuyingController(StockController): 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) + return flt(current_tax_amount, self.precision_of("tax_amount", tax.parentfield)) 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) + return flt(item_tax_map.get(tax.account_head), self.precision_of("rate", tax.parentfield)) else: return tax.rate @@ -350,7 +350,7 @@ class BuyingController(StockController): if tax.category in ["Valuation", "Valuation and Total"] and \ item.item_code in self.stock_items: item.item_tax_amount += flt(current_tax_amount, - self.precision.item.item_tax_amount) + self.precision_of("item_tax_amount", item.parentfield)) # update valuation rate def update_valuation_rate(self, parentfield): @@ -427,18 +427,6 @@ class BuyingController(StockController): return bom_items - - @property - def precision(self): - if not hasattr(self, "_precision"): - self._precision = webnotes._dict() - self._precision.main = self.meta.get_precision_map() - self._precision.item = self.meta.get_precision_map(parentfield = self.fname) - if self.meta.get_field("purchase_tax_details"): - self._precision.tax = self.meta.get_precision_map(parentfield = \ - "purchase_tax_details") - return self._precision - @property def sub_contracted_items(self): if not hasattr(self, "_sub_contracted_items"): diff --git a/controllers/selling_controller.py b/controllers/selling_controller.py index b22042d0fe..b055ca4367 100644 --- a/controllers/selling_controller.py +++ b/controllers/selling_controller.py @@ -16,9 +16,10 @@ from __future__ import unicode_literals import webnotes -from webnotes.utils import cint +from webnotes.utils import cint, flt from setup.utils import get_company_currency from webnotes import msgprint, _ +import json from controllers.stock_controller import StockController @@ -70,4 +71,280 @@ class SellingController(StockController): if item.buying_amount and not item.cost_center: msgprint(_("""Cost Center is mandatory for item: """) + item.item_code, - raise_exception=1) \ No newline at end of file + raise_exception=1) + + def calculate_taxes_and_totals(self): + self.doc.conversion_rate = flt(self.doc.conversion_rate) + self.item_doclist = self.doclist.get({"parentfield": self.fname}) + self.tax_doclist = self.doclist.get({"parentfield": "other_charges"}) + + self.calculate_item_values() + self.initialize_taxes() + + self.determin_exclusive_rate() + + # TODO + # code: save net_total_export on client side + # print format: show net_total_export instead of net_total + + self.calculate_net_total() + self.calculate_taxes() + self.calculate_totals() + # self.calculate_outstanding_amount() + # + self._cleanup() + + def determin_exclusive_rate(self): + if not any((cint(tax.included_in_print_rate) for tax in self.tax_doclist)): + # no inclusive tax + 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.basic_rate = flt((item.export_rate * self.doc.conversion_rate) / + (1 + cumulated_tax_fraction), self.precision_of("basic_rate", item.parentfield)) + + item.amount = flt(item.basic_rate * item.qty, self.precision_of("amount", item.parentfield)) + + item.base_ref_rate = flt(item.basic_rate / (1 - (item.adj_rate / 100.0)), + self.precision_of("base_ref_rate", item.parentfield)) + + 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 + + return current_tax_fraction + + 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_of(print_field, item.parentfield)) * self.doc.conversion_rate), + self.precision_of(base_field, item.parentfield)) + + for item in self.item_doclist: + self.round_floats_in_doc(item, item.parentfield) + + if item.adj_rate == 100: + item.ref_rate = item.ref_rate or item.export_rate + item.export_rate = 0 + else: + if item.ref_rate: + item.export_rate = flt(item.ref_rate * (1.0 - (item.adj_rate / 100.0)), + self.precision_of("export_rate", item.parentfield)) + else: + # assume that print rate and discount are specified + item.ref_rate = flt(item.export_rate / (1.0 - (item.adj_rate / 100.0)), + self.precision_of("ref_rate", item.parentfield)) + + item.export_amount = flt(item.export_rate * item.qty, + self.precision_of("export_amount", item.parentfield)) + + _set_base(item, "ref_rate", "base_ref_rate") + _set_base(item, "export_rate", "basic_rate") + _set_base(item, "export_amount", "amount") + + def initialize_taxes(self): + for tax in self.tax_doclist: + tax.tax_amount = tax.total = 0.0 + # temporary fields + tax.tax_amount_for_current_item = tax.grand_total_for_current_item = 0.0 + tax.item_wise_tax_detail = {} + self.validate_on_previous_row(tax) + self.validate_inclusive_tax(tax) + self.round_floats_in_doc(tax, tax.parentfield) + + def calculate_net_total(self): + self.doc.net_total = 0 + self.doc.net_total_export = 0 + + for item in self.item_doclist: + self.doc.net_total += item.amount + self.doc.net_total_export += item.export_amount + + self.doc.net_total = flt(self.doc.net_total, self.precision_of("net_total")) + self.doc.net_total_export = flt(self.doc.net_total_export, + self.precision_of("net_total_export")) + + def calculate_taxes(self): + for item in self.item_doclist: + item_tax_map = self._load_item_tax_rate(item.item_tax_rate) + + 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) + + # 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.rate, self.precision_of("tax_amount", tax.parentfield)) + 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 + + # 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_of("total", tax.parentfield)) + + else: + tax.grand_total_for_current_item = \ + flt(self.tax_doclist[i-1].grand_total_for_current_item + + current_tax_amount, self.precision_of("total", tax.parentfield)) + + # in tax.total, accumulate grand total of each item + tax.total += tax.grand_total_for_current_item + + # store tax_breakup for each item + # DOUBT: should valuation type amount also be stored? + tax.item_wise_tax_detail[item.item_code] = current_tax_amount + + def calculate_totals(self): + self.doc.grand_total = flt(self.tax_doclist and \ + self.tax_doclist[-1].total or self.doc.net_total, self.precision_of("grand_total")) + self.doc.grand_total_export = flt(self.doc.grand_total / self.doc.conversion_rate, + self.precision_of("grand_total_export")) + + self.doc.rounded_total = round(self.doc.grand_total) + self.doc.rounded_total_export = round(self.doc.grand_total_export) + + 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, self.precision_of("tax_amount", tax.parentfield)) + 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_of("tax_amount", tax.parentfield)) + + 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="other_charges") + }, raise_exception=True) + + def validate_inclusive_tax(self, tax): + def _on_previous_row_error(tax, row_range): + msgprint((_("Row") + + " # %(idx)s [%(taxes_doctype)s] [%(charge_type_label)s = \"%(charge_type)s\"]: " + + _("If:") + ' "%(inclusive_label)s" = ' + _("checked") + ", " + + _("then it is required that:") + " [" + _("Row") + " # %(row_range)s] " + + '"%(inclusive_label)s" = ' + _("checked")) % { + "idx": tax.idx, + "taxes_doctype": tax.doctype, + "inclusive_label": self.meta.get_label("included_in_print_rate", + parentfield="other_charges"), + "charge_type_label": self.meta.get_label("charge_type", + parentfield="other_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": + # inclusive cannot be of 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": tax.doctype, + "charge_type_label": self.meta.get_label("charge_type", + parentfield="other_charges"), + "charge_type": tax.charge_type, + }, raise_exception=True) + elif tax.charge_type == "On Previous Row Amount" and \ + not cint(self.tax_doclist[tax.row_id - 1].included_in_print_rate): + # referred 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]]): + # all rows about this tax should be inclusive + _on_previous_row_error(tax, "1 - %d" % (tax.idx - 1,)) + + 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_of("rate", tax.parentfield)) + else: + return tax.rate + + def _cleanup(self): + for tax in self.tax_doclist: + del tax.fields["grand_total_for_current_item"] + del tax.fields["tax_amount_for_current_item"] + + for fieldname in ("tax_fraction_for_current_item", + "grand_total_fraction_for_current_item"): + if fieldname in tax.fields: + del tax.fields[fieldname] + + tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail) diff --git a/patches/may_2013/__init__.py b/patches/may_2013/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/patches/may_2013/p01_selling_net_total_export.py b/patches/may_2013/p01_selling_net_total_export.py new file mode 100644 index 0000000000..dd0f68ac0a --- /dev/null +++ b/patches/may_2013/p01_selling_net_total_export.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import webnotes + +def execute(): + for module, doctype in (("Accounts", "Sales Invoice"), ("Selling", "Sales Order"), ("Selling", "Quotation"), + ("Stock", "Delivery Note")): + webnotes.reload_doc(module, "DocType", doctype) + webnotes.conn.sql("""update `tab%s` + set net_total_export = round(net_total / if(conversion_rate=0, 1, ifnull(conversion_rate, 1)), 2)""" % + (doctype,)) \ No newline at end of file diff --git a/patches/patch_list.py b/patches/patch_list.py index 432f8f9d35..0a1370e453 100644 --- a/patches/patch_list.py +++ b/patches/patch_list.py @@ -250,4 +250,5 @@ patch_list = [ "patches.april_2013.p07_update_file_data_2", "patches.april_2013.rebuild_sales_browser", "patches.april_2013.p08_price_list_country", + "patches.may_2013.p01_selling_net_total_export", ] \ No newline at end of file diff --git a/selling/doctype/quotation/quotation.txt b/selling/doctype/quotation/quotation.txt index feda14c591..24a080bac1 100644 --- a/selling/doctype/quotation/quotation.txt +++ b/selling/doctype/quotation/quotation.txt @@ -1,8 +1,8 @@ [ { - "creation": "2013-04-03 09:10:44", + "creation": "2013-05-06 12:03:40", "docstatus": 0, - "modified": "2013-04-03 09:58:02", + "modified": "2013-05-06 13:07:37", "modified_by": "Administrator", "owner": "Administrator" }, @@ -239,11 +239,19 @@ "oldfieldname": "net_total", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "print_hide": 0, + "print_hide": 1, "read_only": 1, "reqd": 0, "width": "100px" }, + { + "doctype": "DocField", + "fieldname": "net_total_export", + "fieldtype": "Currency", + "label": "Net Total (Export)", + "options": "currency", + "read_only": 1 + }, { "doctype": "DocField", "fieldname": "recalculate_values", diff --git a/selling/doctype/sales_common/sales_common.js b/selling/doctype/sales_common/sales_common.js index 8a8d8d0a6c..1d020e6780 100644 --- a/selling/doctype/sales_common/sales_common.js +++ b/selling/doctype/sales_common/sales_common.js @@ -518,6 +518,8 @@ cur_frm.cscript.calc_doc_values = function(doc, cdt, cdn, tname, fname, other_fn if(flt(doc.conversion_rate)>1) { net_total_incl *= flt(doc.conversion_rate); } + + // TODO: store net_total_export doc.net_total = inclusive_rate ? flt(net_total_incl) : flt(net_total); doc.other_charges_total = roundNumber(flt(other_charges_total), 2); diff --git a/selling/doctype/sales_order/sales_order.txt b/selling/doctype/sales_order/sales_order.txt index ba0b1de07c..9780dc71a3 100644 --- a/selling/doctype/sales_order/sales_order.txt +++ b/selling/doctype/sales_order/sales_order.txt @@ -1,8 +1,8 @@ [ { - "creation": "2013-03-07 14:48:34", + "creation": "2013-05-06 12:03:43", "docstatus": 0, - "modified": "2013-01-29 17:14:58", + "modified": "2013-05-06 13:06:37", "modified_by": "Administrator", "owner": "Administrator" }, @@ -251,11 +251,19 @@ "oldfieldname": "net_total", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "print_hide": 0, + "print_hide": 1, "read_only": 1, "reqd": 0, "width": "150px" }, + { + "doctype": "DocField", + "fieldname": "net_total_export", + "fieldtype": "Currency", + "label": "Net Total (Export)", + "options": "currency", + "read_only": 1 + }, { "doctype": "DocField", "fieldname": "recalculate_values", @@ -955,7 +963,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "report": 0, "role": "Sales Manager", @@ -978,7 +985,6 @@ "cancel": 1, "create": 1, "doctype": "DocPerm", - "match": "", "permlevel": 0, "report": 1, "role": "Sales User", @@ -990,7 +996,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "report": 0, "role": "Sales User", @@ -1013,7 +1018,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "role": "Maintenance Manager", "submit": 0 @@ -1034,7 +1038,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "role": "Maintenance User", "submit": 0 diff --git a/stock/doctype/delivery_note/delivery_note.txt b/stock/doctype/delivery_note/delivery_note.txt index 36c2789bfa..6f299efb15 100644 --- a/stock/doctype/delivery_note/delivery_note.txt +++ b/stock/doctype/delivery_note/delivery_note.txt @@ -1,8 +1,8 @@ [ { - "creation": "2013-04-02 10:50:50", + "creation": "2013-05-06 12:03:30", "docstatus": 0, - "modified": "2013-02-02 19:18:38", + "modified": "2013-05-06 13:08:13", "modified_by": "Administrator", "owner": "Administrator" }, @@ -255,12 +255,20 @@ "oldfieldname": "net_total", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "print_hide": 0, + "print_hide": 1, "print_width": "150px", "read_only": 1, "reqd": 0, "width": "150px" }, + { + "doctype": "DocField", + "fieldname": "net_total_export", + "fieldtype": "Currency", + "label": "Net Total (Export)", + "options": "currency", + "read_only": 1 + }, { "doctype": "DocField", "fieldname": "recalculate_values", @@ -1136,7 +1144,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "report": 0, "role": "Material User", @@ -1159,7 +1166,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "report": 0, "role": "Material Manager", @@ -1171,7 +1177,6 @@ "cancel": 1, "create": 1, "doctype": "DocPerm", - "match": "", "permlevel": 0, "report": 1, "role": "Sales User", @@ -1183,7 +1188,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "report": 0, "role": "Sales User", @@ -1205,7 +1209,6 @@ "cancel": 0, "create": 0, "doctype": "DocPerm", - "match": "", "permlevel": 1, "role": "Accounts User", "submit": 0 diff --git a/stock/doctype/stock_entry/test_stock_entry.py b/stock/doctype/stock_entry/test_stock_entry.py index c3ce2d7f40..a9281cd004 100644 --- a/stock/doctype/stock_entry/test_stock_entry.py +++ b/stock/doctype/stock_entry/test_stock_entry.py @@ -450,6 +450,7 @@ class TestStockEntry(unittest.TestCase): for d in pi.doclist.get({"parentfield": "entries"}): d.expense_head = "_Test Account Cost for Goods Sold - _TC" d.cost_center = "_Test Cost Center - _TC" + for d in pi.doclist.get({"parentfield": "purchase_tax_details"}): d.cost_center = "_Test Cost Center - _TC"