cc61daeec4
2 New test cases added. 1. Standalone Cr notes will be reported as normal Invoices 2. Cr notes against an Invoice will not overallocate qty if there are multiple instances of same item
463 lines
13 KiB
Python
463 lines
13 KiB
Python
import frappe
|
|
from frappe import qb
|
|
from frappe.tests.utils import FrappeTestCase
|
|
from frappe.utils import add_days, flt, nowdate
|
|
|
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
|
from erpnext.accounts.report.gross_profit.gross_profit import execute
|
|
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
|
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
|
from erpnext.stock.doctype.item.test_item import create_item
|
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
|
|
|
|
|
class TestGrossProfit(FrappeTestCase):
|
|
def setUp(self):
|
|
self.create_company()
|
|
self.create_item()
|
|
self.create_bundle()
|
|
self.create_customer()
|
|
self.create_sales_invoice()
|
|
self.clear_old_entries()
|
|
|
|
def tearDown(self):
|
|
frappe.db.rollback()
|
|
|
|
def create_company(self):
|
|
company_name = "_Test Gross Profit"
|
|
abbr = "_GP"
|
|
if frappe.db.exists("Company", company_name):
|
|
company = frappe.get_doc("Company", company_name)
|
|
else:
|
|
company = frappe.get_doc(
|
|
{
|
|
"doctype": "Company",
|
|
"company_name": company_name,
|
|
"country": "India",
|
|
"default_currency": "INR",
|
|
"create_chart_of_accounts_based_on": "Standard Template",
|
|
"chart_of_accounts": "Standard",
|
|
}
|
|
)
|
|
company = company.save()
|
|
|
|
self.company = company.name
|
|
self.cost_center = company.cost_center
|
|
self.warehouse = "Stores - " + abbr
|
|
self.finished_warehouse = "Finished Goods - " + abbr
|
|
self.income_account = "Sales - " + abbr
|
|
self.expense_account = "Cost of Goods Sold - " + abbr
|
|
self.debit_to = "Debtors - " + abbr
|
|
self.creditors = "Creditors - " + abbr
|
|
|
|
def create_item(self):
|
|
item = create_item(
|
|
item_code="_Test GP Item", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
|
)
|
|
self.item = item if isinstance(item, str) else item.item_code
|
|
|
|
def create_bundle(self):
|
|
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
|
|
|
item2 = create_item(
|
|
item_code="_Test GP Item 2", is_stock_item=1, company=self.company, warehouse=self.warehouse
|
|
)
|
|
self.item2 = item2 if isinstance(item2, str) else item2.item_code
|
|
|
|
# This will be parent item
|
|
bundle = create_item(
|
|
item_code="_Test GP bundle", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
|
)
|
|
self.bundle = bundle if isinstance(bundle, str) else bundle.item_code
|
|
|
|
# Create Product Bundle
|
|
self.product_bundle = make_product_bundle(parent=self.bundle, items=[self.item, self.item2])
|
|
|
|
def create_customer(self):
|
|
name = "_Test GP Customer"
|
|
if frappe.db.exists("Customer", name):
|
|
self.customer = name
|
|
else:
|
|
customer = frappe.new_doc("Customer")
|
|
customer.customer_name = name
|
|
customer.type = "Individual"
|
|
customer.save()
|
|
self.customer = customer.name
|
|
|
|
def create_sales_invoice(
|
|
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
|
):
|
|
"""
|
|
Helper function to populate default values in sales invoice
|
|
"""
|
|
sinv = create_sales_invoice(
|
|
qty=qty,
|
|
rate=rate,
|
|
company=self.company,
|
|
customer=self.customer,
|
|
item_code=self.item,
|
|
item_name=self.item,
|
|
cost_center=self.cost_center,
|
|
warehouse=self.warehouse,
|
|
debit_to=self.debit_to,
|
|
parent_cost_center=self.cost_center,
|
|
update_stock=0,
|
|
currency="INR",
|
|
is_pos=0,
|
|
is_return=0,
|
|
return_against=None,
|
|
income_account=self.income_account,
|
|
expense_account=self.expense_account,
|
|
do_not_save=do_not_save,
|
|
do_not_submit=do_not_submit,
|
|
)
|
|
return sinv
|
|
|
|
def create_delivery_note(
|
|
self, item=None, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
|
):
|
|
"""
|
|
Helper function to populate default values in Delivery Note
|
|
"""
|
|
dnote = create_delivery_note(
|
|
company=self.company,
|
|
customer=self.customer,
|
|
currency="INR",
|
|
item=item or self.item,
|
|
qty=qty,
|
|
rate=rate,
|
|
cost_center=self.cost_center,
|
|
warehouse=self.warehouse,
|
|
return_against=None,
|
|
expense_account=self.expense_account,
|
|
do_not_save=do_not_save,
|
|
do_not_submit=do_not_submit,
|
|
)
|
|
return dnote
|
|
|
|
def clear_old_entries(self):
|
|
doctype_list = [
|
|
"Sales Invoice",
|
|
"GL Entry",
|
|
"Payment Ledger Entry",
|
|
"Stock Entry",
|
|
"Stock Ledger Entry",
|
|
"Delivery Note",
|
|
]
|
|
for doctype in doctype_list:
|
|
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
|
|
|
def test_invoice_without_only_delivery_note(self):
|
|
"""
|
|
Test buying amount for Invoice without `update_stock` flag set but has Delivery Note
|
|
"""
|
|
se = make_stock_entry(
|
|
company=self.company,
|
|
item_code=self.item,
|
|
target=self.warehouse,
|
|
qty=1,
|
|
basic_rate=100,
|
|
do_not_submit=True,
|
|
)
|
|
item = se.items[0]
|
|
se.append(
|
|
"items",
|
|
{
|
|
"item_code": item.item_code,
|
|
"s_warehouse": item.s_warehouse,
|
|
"t_warehouse": item.t_warehouse,
|
|
"qty": 1,
|
|
"basic_rate": 200,
|
|
"conversion_factor": item.conversion_factor or 1.0,
|
|
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
|
"serial_no": item.serial_no,
|
|
"batch_no": item.batch_no,
|
|
"cost_center": item.cost_center,
|
|
"expense_account": item.expense_account,
|
|
},
|
|
)
|
|
se = se.save().submit()
|
|
|
|
sinv = create_sales_invoice(
|
|
qty=1,
|
|
rate=100,
|
|
company=self.company,
|
|
customer=self.customer,
|
|
item_code=self.item,
|
|
item_name=self.item,
|
|
cost_center=self.cost_center,
|
|
warehouse=self.warehouse,
|
|
debit_to=self.debit_to,
|
|
parent_cost_center=self.cost_center,
|
|
update_stock=0,
|
|
currency="INR",
|
|
income_account=self.income_account,
|
|
expense_account=self.expense_account,
|
|
)
|
|
|
|
filters = frappe._dict(
|
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
|
)
|
|
|
|
columns, data = execute(filters=filters)
|
|
|
|
# Without Delivery Note, buying rate should be 150
|
|
expected_entry_without_dn = {
|
|
"parent_invoice": sinv.name,
|
|
"currency": "INR",
|
|
"sales_invoice": self.item,
|
|
"customer": self.customer,
|
|
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
|
"item_code": self.item,
|
|
"item_name": self.item,
|
|
"warehouse": "Stores - _GP",
|
|
"qty": 1.0,
|
|
"avg._selling_rate": 100.0,
|
|
"valuation_rate": 150.0,
|
|
"selling_amount": 100.0,
|
|
"buying_amount": 150.0,
|
|
"gross_profit": -50.0,
|
|
"gross_profit_%": -50.0,
|
|
}
|
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
|
self.assertDictContainsSubset(expected_entry_without_dn, gp_entry[0])
|
|
|
|
# make delivery note
|
|
dn = make_delivery_note(sinv.name)
|
|
dn.items[0].qty = 1
|
|
dn = dn.save().submit()
|
|
|
|
columns, data = execute(filters=filters)
|
|
|
|
# Without Delivery Note, buying rate should be 100
|
|
expected_entry_with_dn = {
|
|
"parent_invoice": sinv.name,
|
|
"currency": "INR",
|
|
"sales_invoice": self.item,
|
|
"customer": self.customer,
|
|
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
|
"item_code": self.item,
|
|
"item_name": self.item,
|
|
"warehouse": "Stores - _GP",
|
|
"qty": 1.0,
|
|
"avg._selling_rate": 100.0,
|
|
"valuation_rate": 100.0,
|
|
"selling_amount": 100.0,
|
|
"buying_amount": 100.0,
|
|
"gross_profit": 0.0,
|
|
"gross_profit_%": 0.0,
|
|
}
|
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
|
self.assertDictContainsSubset(expected_entry_with_dn, gp_entry[0])
|
|
|
|
def test_bundled_delivery_note_with_different_warehouses(self):
|
|
"""
|
|
Test Delivery Note with bundled item. Packed Item from the bundle having different warehouses
|
|
"""
|
|
se = make_stock_entry(
|
|
company=self.company,
|
|
item_code=self.item,
|
|
target=self.warehouse,
|
|
qty=1,
|
|
basic_rate=100,
|
|
do_not_submit=True,
|
|
)
|
|
item = se.items[0]
|
|
se.append(
|
|
"items",
|
|
{
|
|
"item_code": self.item2,
|
|
"s_warehouse": "",
|
|
"t_warehouse": self.finished_warehouse,
|
|
"qty": 1,
|
|
"basic_rate": 100,
|
|
"conversion_factor": item.conversion_factor or 1.0,
|
|
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
|
"serial_no": item.serial_no,
|
|
"batch_no": item.batch_no,
|
|
"cost_center": item.cost_center,
|
|
"expense_account": item.expense_account,
|
|
},
|
|
)
|
|
se = se.save().submit()
|
|
|
|
# Make a Delivery note with Product bundle
|
|
# Packed Items will have different warehouses
|
|
dnote = self.create_delivery_note(item=self.bundle, qty=1, rate=200, do_not_submit=True)
|
|
dnote.packed_items[1].warehouse = self.finished_warehouse
|
|
dnote = dnote.submit()
|
|
|
|
# make Sales Invoice for above delivery note
|
|
sinv = make_sales_invoice(dnote.name)
|
|
sinv = sinv.save().submit()
|
|
|
|
filters = frappe._dict(
|
|
company=self.company,
|
|
from_date=nowdate(),
|
|
to_date=nowdate(),
|
|
group_by="Invoice",
|
|
sales_invoice=sinv.name,
|
|
)
|
|
|
|
columns, data = execute(filters=filters)
|
|
self.assertGreater(len(data), 0)
|
|
|
|
def test_order_connected_dn_and_inv(self):
|
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
|
|
|
"""
|
|
Test gp calculation when invoice and delivery note aren't directly connected.
|
|
SO -- INV
|
|
|
|
|
DN
|
|
"""
|
|
se = make_stock_entry(
|
|
company=self.company,
|
|
item_code=self.item,
|
|
target=self.warehouse,
|
|
qty=3,
|
|
basic_rate=100,
|
|
do_not_submit=True,
|
|
)
|
|
item = se.items[0]
|
|
se.append(
|
|
"items",
|
|
{
|
|
"item_code": item.item_code,
|
|
"s_warehouse": item.s_warehouse,
|
|
"t_warehouse": item.t_warehouse,
|
|
"qty": 10,
|
|
"basic_rate": 200,
|
|
"conversion_factor": item.conversion_factor or 1.0,
|
|
"transfer_qty": flt(item.qty) * (flt(item.conversion_factor) or 1.0),
|
|
"serial_no": item.serial_no,
|
|
"batch_no": item.batch_no,
|
|
"cost_center": item.cost_center,
|
|
"expense_account": item.expense_account,
|
|
},
|
|
)
|
|
se = se.save().submit()
|
|
|
|
so = make_sales_order(
|
|
customer=self.customer,
|
|
company=self.company,
|
|
warehouse=self.warehouse,
|
|
item=self.item,
|
|
qty=4,
|
|
do_not_save=False,
|
|
do_not_submit=False,
|
|
)
|
|
|
|
from erpnext.selling.doctype.sales_order.sales_order import (
|
|
make_delivery_note,
|
|
make_sales_invoice,
|
|
)
|
|
|
|
make_delivery_note(so.name).submit()
|
|
sinv = make_sales_invoice(so.name).submit()
|
|
|
|
filters = frappe._dict(
|
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
|
)
|
|
|
|
columns, data = execute(filters=filters)
|
|
expected_entry = {
|
|
"parent_invoice": sinv.name,
|
|
"currency": "INR",
|
|
"sales_invoice": self.item,
|
|
"customer": self.customer,
|
|
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
|
"item_code": self.item,
|
|
"item_name": self.item,
|
|
"warehouse": "Stores - _GP",
|
|
"qty": 4.0,
|
|
"avg._selling_rate": 100.0,
|
|
"valuation_rate": 125.0,
|
|
"selling_amount": 400.0,
|
|
"buying_amount": 500.0,
|
|
"gross_profit": -100.0,
|
|
"gross_profit_%": -25.0,
|
|
}
|
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
|
|
|
def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
|
|
"""
|
|
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
|
"""
|
|
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
|
|
|
# Invoice with an item added twice
|
|
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
|
sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
|
|
sinv = sinv.save().submit()
|
|
|
|
# Create Credit Note for Invoice
|
|
cr_note = make_sales_return(sinv.name)
|
|
cr_note = cr_note.save().submit()
|
|
|
|
filters = frappe._dict(
|
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
|
)
|
|
|
|
columns, data = execute(filters=filters)
|
|
expected_entry = {
|
|
"parent_invoice": sinv.name,
|
|
"currency": "INR",
|
|
"sales_invoice": self.item,
|
|
"customer": self.customer,
|
|
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
|
"item_code": self.item,
|
|
"item_name": self.item,
|
|
"warehouse": "Stores - _GP",
|
|
"qty": 0.0,
|
|
"avg._selling_rate": 0.0,
|
|
"valuation_rate": 0.0,
|
|
"selling_amount": -100.0,
|
|
"buying_amount": 0.0,
|
|
"gross_profit": -100.0,
|
|
"gross_profit_%": 100.0,
|
|
}
|
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
|
# Both items of Invoice should have '0' qty
|
|
self.assertEqual(len(gp_entry), 2)
|
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
|
self.assertDictContainsSubset(expected_entry, gp_entry[1])
|
|
|
|
def test_standalone_cr_notes(self):
|
|
"""
|
|
Standalone cr notes will be reported as usual
|
|
"""
|
|
# Make Cr Note
|
|
sinv = self.create_sales_invoice(
|
|
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
|
)
|
|
sinv.is_return = 1
|
|
sinv = sinv.save().submit()
|
|
|
|
filters = frappe._dict(
|
|
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
|
)
|
|
|
|
columns, data = execute(filters=filters)
|
|
expected_entry = {
|
|
"parent_invoice": sinv.name,
|
|
"currency": "INR",
|
|
"sales_invoice": self.item,
|
|
"customer": self.customer,
|
|
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
|
"item_code": self.item,
|
|
"item_name": self.item,
|
|
"warehouse": "Stores - _GP",
|
|
"qty": -1.0,
|
|
"avg._selling_rate": 100.0,
|
|
"valuation_rate": 0.0,
|
|
"selling_amount": -100.0,
|
|
"buying_amount": 0.0,
|
|
"gross_profit": -100.0,
|
|
"gross_profit_%": 100.0,
|
|
}
|
|
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
|
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|