diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 5a5c448026e..199a183e479 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -109,7 +109,7 @@ def get_region(company=None):
'''
if company or frappe.flags.company:
return frappe.get_cached_value('Company',
- company or frappe.flags.company, 'country')
+ company or frappe.flags.company, 'country')
elif frappe.flags.country:
return frappe.flags.country
else:
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 94573f9df3c..76e00923c49 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -179,10 +179,18 @@ class POSInvoice(SalesInvoice):
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
- serial_no_exists = frappe.db.exists("POS Invoice Item", {
- "parent": self.return_against,
- "serial_no": ["like", d.get("serial_no")]
- })
+ serial_no_exists = frappe.db.sql("""
+ SELECT name
+ FROM `tabPOS Invoice Item`
+ WHERE
+ parent = %s
+ and (serial_no = %s
+ or serial_no like %s
+ or serial_no like %s
+ or serial_no like %s
+ )
+ """, (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%'))
+
if not serial_no_exists:
bold_return_against = frappe.bold(self.return_against)
bold_serial_no = frappe.bold(sr)
@@ -190,7 +198,7 @@ class POSInvoice(SalesInvoice):
_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
.format(d.idx, bold_serial_no, bold_return_against)
)
-
+
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
@@ -292,7 +300,7 @@ class POSInvoice(SalesInvoice):
if not self.get('payments') and not for_validate:
update_multi_mode_option(self, profile)
-
+
if self.is_return and not for_validate:
add_return_modes(self, profile)
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 57a23af8af0..15875afe878 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -198,6 +198,65 @@ class TestPOSInvoice(unittest.TestCase):
self.assertEqual(pos_return.get('payments')[0].amount, -500)
self.assertEqual(pos_return.get('payments')[1].amount, -500)
+ def test_pos_return_for_serialized_item(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ se = make_serialized_item(company='_Test Company',
+ target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+ serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+ pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+ account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+ expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+ item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+
+ pos.get("items")[0].serial_no = serial_nos[0]
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+ pos.insert()
+ pos.submit()
+
+ pos_return = make_sales_return(pos.name)
+
+ pos_return.insert()
+ pos_return.submit()
+ self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0])
+
+ def test_partial_pos_returns(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ se = make_serialized_item(company='_Test Company',
+ target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+ serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+ pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+ account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+ expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+ item=se.get("items")[0].item_code, qty=2, rate=1000, do_not_save=1)
+
+ pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1]
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+ pos.insert()
+ pos.submit()
+
+ pos_return1 = make_sales_return(pos.name)
+
+ # partial return 1
+ pos_return1.get('items')[0].qty = -1
+ pos_return1.get('items')[0].serial_no = serial_nos[0]
+ pos_return1.insert()
+ pos_return1.submit()
+
+ # partial return 2
+ pos_return2 = make_sales_return(pos.name)
+ self.assertEqual(pos_return2.get('items')[0].qty, -1)
+ self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1])
+
def test_pos_change_amount(self):
pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 2b6e7de118a..8b71eb02fd7 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -87,6 +87,7 @@
"edit_references",
"sales_order",
"so_detail",
+ "pos_invoice_item",
"column_break_74",
"delivery_note",
"dn_detail",
@@ -790,11 +791,20 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
+ },
+ {
+ "fieldname": "pos_invoice_item",
+ "fieldtype": "Data",
+ "ignore_user_permissions": 1,
+ "label": "POS Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-07-22 13:40:34.418346",
+ "modified": "2021-01-04 17:34:49.924531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index c88d67989be..40f77b4088d 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -29,7 +29,7 @@ class POSInvoiceMergeLog(Document):
for d in self.pos_invoices:
status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
-
+
bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status)
if docstatus != 1:
@@ -58,7 +58,7 @@ class POSInvoiceMergeLog(Document):
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
-
+
if returns:
credit_note = self.process_merging_into_credit_note(returns)
@@ -74,7 +74,7 @@ class POSInvoiceMergeLog(Document):
def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice()
-
+
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
@@ -98,19 +98,19 @@ class POSInvoiceMergeLog(Document):
self.consolidated_credit_note = credit_note.name
return credit_note.name
-
+
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0
for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
-
+
if doc.redeem_loyalty_points:
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
loyalty_points_sum += doc.loyalty_points
loyalty_amount_sum += doc.loyalty_amount
-
+
for item in doc.get('items'):
found = False
for i in items:
@@ -118,12 +118,13 @@ class POSInvoiceMergeLog(Document):
i.uom == item.uom and i.net_rate == item.net_rate):
found = True
i.qty = i.qty + item.qty
+
if not found:
item.rate = item.net_rate
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item)
-
+
for tax in doc.get('taxes'):
found = False
for t in taxes:
@@ -162,7 +163,7 @@ class POSInvoiceMergeLog(Document):
invoice.ignore_pricing_rule = 1
return invoice
-
+
def get_new_sales_invoice(self):
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer
@@ -194,7 +195,7 @@ def get_all_unconsolidated_invoices():
}
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
-
+
return pos_invoices
def get_invoice_customer_map(pos_invoices):
@@ -204,7 +205,7 @@ def get_invoice_customer_map(pos_invoices):
customer = invoice.get('customer')
pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice)
-
+
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index d0116890b65..76f3c50578e 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -222,7 +222,7 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
set_gl_entries_by_account(start_date,
end_date, root.lft, root.rgt, filters,
- gl_entries_by_account, accounts_by_name, ignore_closing_entries=False)
+ gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
@@ -339,7 +339,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
return data
def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account,
- accounts_by_name, ignore_closing_entries=False):
+ accounts_by_name, accounts, ignore_closing_entries=False):
"""Returns a dict like { "account": [gl entries], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company',
@@ -382,15 +382,31 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
for entry in gl_entries:
key = entry.account_number or entry.account_name
- validate_entries(key, entry, accounts_by_name)
+ validate_entries(key, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry)
return gl_entries_by_account
-def validate_entries(key, entry, accounts_by_name):
+def get_account_details(account):
+ return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company',
+ 'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
+
+def validate_entries(key, entry, accounts_by_name, accounts):
if key not in accounts_by_name:
- field = "Account number" if entry.account_number else "Account name"
- frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
+ args = get_account_details(entry.account)
+
+ if args.parent_account:
+ parent_args = get_account_details(args.parent_account)
+
+ args.update({
+ 'lft': parent_args.lft + 1,
+ 'rgt': parent_args.rgt - 1,
+ 'root_type': parent_args.root_type,
+ 'report_type': parent_args.report_type
+ })
+
+ accounts_by_name.setdefault(key, args)
+ accounts.append(args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 67c7fd2d22a..60d1e20feaf 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -82,7 +82,7 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
-
+
if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg)
@@ -888,17 +888,22 @@ def get_coa(doctype, parent, is_root, chart=None):
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=None):
+ stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items, company)
+ repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account)
+
+
+def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, warehouse_account=None):
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
+
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
- future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items)
- gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
+ gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
- for voucher_type, voucher_no in future_stock_vouchers:
+ for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
@@ -909,7 +914,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
else:
_delete_gl_entries(voucher_type, voucher_no)
-def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None):
+def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
future_stock_vouchers = []
values = []
@@ -922,6 +927,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += for_warehouses
+ if company:
+ condition += " and company = %s"
+ values.append(company)
+
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where
@@ -982,7 +991,7 @@ def check_if_stock_and_account_balance_synced(posting_date, company, voucher_typ
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format(
stock_bal, account_bal, frappe.bold(account), posting_date)
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\
- .format(frappe.bold(diff), frappe.bold(posting_date))
+ .format(frappe.bold(diff), frappe.bold(posting_date))
frappe.msgprint(
msg="""{0}
{1}
""".format(error_reason, error_resolution),
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index a51498e9354..7cf22f87e4f 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -127,6 +127,10 @@ class RequestforQuotation(BuyingController):
'link_doctype': 'Supplier',
'link_name': rfq_supplier.supplier
})
+ contact.append('email_ids', {
+ 'email_id': user.name,
+ 'is_primary': 1
+ })
if not contact.email_id and not contact.user:
contact.email_id = user.name
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 40362b1d404..4cc5753cbd0 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -26,7 +26,6 @@
"supplier_group",
"supplier_type",
"pan",
- "language",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"disabled",
@@ -57,6 +56,7 @@
"website",
"supplier_details",
"column_break_30",
+ "language",
"is_frozen"
],
"fields": [
@@ -384,7 +384,7 @@
"idx": 370,
"image_field": "image",
"links": [],
- "modified": "2020-06-17 23:18:20",
+ "modified": "2021-01-06 19:51:40.939087",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 991eef1d218..12a81c78879 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1309,45 +1309,28 @@ def add_taxes_from_tax_template(child_item, parent_doc):
})
tax_row.db_insert()
-def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
+def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item):
"""
- Returns a Sales Order Item child item containing the default values
+ Returns a Sales/Purchase Order Item child item containing the default values
"""
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
- child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname)
+ child_item = frappe.new_doc(child_doctype, p_doc, child_docname)
item = frappe.get_doc("Item", trans_item.get('item_code'))
- child_item.item_code = item.item_code
- child_item.item_name = item.item_name
- child_item.description = item.description
- child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
+ for field in ("item_code", "item_name", "description", "item_group"):
+ child_item.update({field: item.get(field)})
+ date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
+ child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
child_item.uom = trans_item.get("uom") or item.stock_uom
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
- set_child_tax_template_and_map(item, child_item, p_doc)
- add_taxes_from_tax_template(child_item, p_doc)
- child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
- if not child_item.warehouse:
- frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
- .format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
- return child_item
-
-
-def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
- """
- Returns a Purchase Order Item child item containing the default values
- """
- p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
- child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname)
- item = frappe.get_doc("Item", trans_item.get('item_code'))
- child_item.item_code = item.item_code
- child_item.item_name = item.item_name
- child_item.description = item.description
- child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date
- child_item.uom = trans_item.get("uom") or item.stock_uom
- conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
- child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
- child_item.base_rate = 1 # Initiallize value will update in parent validation
- child_item.base_amount = 1 # Initiallize value will update in parent validation
+ if child_doctype == "Purchase Order Item":
+ child_item.base_rate = 1 # Initiallize value will update in parent validation
+ child_item.base_amount = 1 # Initiallize value will update in parent validation
+ if child_doctype == "Sales Order Item":
+ child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
+ if not child_item.warehouse:
+ frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
+ .format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc)
return child_item
@@ -1411,8 +1394,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
)
def get_new_child_item(item_row):
- new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
- return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row)
+ child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
+ return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 0e1829a7676..de61b35316e 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -204,8 +204,6 @@ def get_already_returned_items(doc):
return items
def get_returned_qty_map_for_row(row_name, doctype):
- if doctype == "POS Invoice": return {}
-
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
@@ -354,7 +352,12 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account
- target_doc.sales_invoice_item = source_doc.name
+
+ if doctype == "Sales Invoice":
+ target_doc.sales_invoice_item = source_doc.name
+ else:
+ target_doc.pos_invoice_item = source_doc.name
+
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 70e8d8cc785..ea9659ce017 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -488,13 +488,12 @@ class StockController(AccountsController):
"voucher_no": self.name,
"company": self.company
})
-
if check_if_future_sle_exists(args):
create_repost_item_valuation_entry(args)
elif not is_reposting_pending():
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
-
+
def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index cfa499191ca..10271cbcc9d 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -15,6 +15,8 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_ra
class calculate_taxes_and_totals(object):
def __init__(self, doc):
self.doc = doc
+ frappe.flags.round_off_applicable_accounts = []
+ get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
def calculate(self):
@@ -332,10 +334,18 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
+ current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
+ def get_final_current_tax_amount(self, tax, current_tax_amount):
+ # Some countries need individual tax components to be rounded
+ # Handeled via regional doctypess
+ if tax.account_head in frappe.flags.round_off_applicable_accounts:
+ current_tax_amount = round(current_tax_amount, 0)
+ return current_tax_amount
+
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item
key = item.item_code or item.item_name
@@ -693,6 +703,15 @@ def get_itemised_tax_breakup_html(doc):
)
)
+@frappe.whitelist()
+def get_round_off_applicable_accounts(company, account_list):
+ account_list = get_regional_round_off_accounts(company, account_list)
+
+ return account_list
+
+@erpnext.allow_regional
+def get_regional_round_off_accounts(company, account_list):
+ pass
@erpnext.allow_regional
def update_itemised_tax_data(doc):
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 2df1793fdbe..1b33fd73acf 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -49,6 +49,7 @@
"phone",
"mobile_no",
"fax",
+ "website",
"more_info",
"type",
"market_segment",
@@ -56,8 +57,8 @@
"request_type",
"column_break3",
"company",
- "website",
"territory",
+ "language",
"unsubscribed",
"blog_subscriber",
"title"
@@ -447,13 +448,19 @@
"fieldtype": "Select",
"label": "Address Type",
"options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther"
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2020-10-13 15:24:00.094811",
+ "modified": "2021-01-06 19:39:58.748978",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index eee13f7e799..2e09a76c0f6 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -54,6 +54,7 @@
"campaign",
"column_break1",
"transaction_date",
+ "language",
"amended_from",
"lost_reasons"
],
@@ -419,12 +420,18 @@
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2020-08-12 17:34:35.066961",
+ "modified": "2021-01-06 19:42:46.190051",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.js b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
index 15916a5134a..861675acea3 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.js
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
@@ -2,4 +2,82 @@
// For license information, please see license.txt
frappe.ui.form.on('Appointment Type', {
+ refresh: function(frm) {
+ frm.set_query('price_list', function() {
+ return {
+ filters: {'selling': 1}
+ };
+ });
+
+ frm.set_query('medical_department', 'items', function(doc) {
+ let item_list = doc.items.map(({medical_department}) => medical_department);
+ return {
+ filters: [
+ ['Medical Department', 'name', 'not in', item_list]
+ ]
+ };
+ });
+
+ frm.set_query('op_consulting_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+
+ frm.set_query('inpatient_visit_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+ }
});
+
+frappe.ui.form.on('Appointment Type Service Item', {
+ op_consulting_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.op_consulting_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.op_consulting_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function(data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ },
+
+ inpatient_visit_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.inpatient_visit_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.inpatient_visit_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function (data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.json b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
index 58753bb4f05..38723182878 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.json
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
@@ -12,7 +12,10 @@
"appointment_type",
"ip",
"default_duration",
- "color"
+ "color",
+ "billing_section",
+ "price_list",
+ "items"
],
"fields": [
{
@@ -52,10 +55,27 @@
"label": "Color",
"no_copy": 1,
"report_hide": 1
+ },
+ {
+ "fieldname": "billing_section",
+ "fieldtype": "Section Break",
+ "label": "Billing"
+ },
+ {
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Appointment Type Service Items",
+ "options": "Appointment Type Service Item"
}
],
"links": [],
- "modified": "2020-02-03 21:06:05.833050",
+ "modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.py b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
index 1dacffab357..67a24f31e03 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.py
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
@@ -4,6 +4,53 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+import frappe
class AppointmentType(Document):
- pass
+ def validate(self):
+ if self.items and self.price_list:
+ for item in self.items:
+ existing_op_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.op_consulting_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge:
+ make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge)
+
+ existing_ip_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.inpatient_visit_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge:
+ make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge)
+
+@frappe.whitelist()
+def get_service_item_based_on_department(appointment_type, department):
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'medical_department': department, 'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ # if department wise items are not set up
+ # use the generic items
+ if not item_list:
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ return item_list
+
+def make_item_price(price_list, item, item_price):
+ frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'price_list': price_list,
+ 'item_code': item,
+ 'price_list_rate': item_price
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
new file mode 100644
index 00000000000..5ff68cd682c
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
@@ -0,0 +1,67 @@
+{
+ "actions": [],
+ "creation": "2021-01-22 09:34:53.373105",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "medical_department",
+ "op_consulting_charge_item",
+ "op_consulting_charge",
+ "column_break_4",
+ "inpatient_visit_charge_item",
+ "inpatient_visit_charge"
+ ],
+ "fields": [
+ {
+ "fieldname": "medical_department",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Medical Department",
+ "options": "Medical Department"
+ },
+ {
+ "fieldname": "op_consulting_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "op_consulting_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "inpatient_visit_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "inpatient_visit_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-01-22 09:35:26.503443",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Appointment Type Service Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
new file mode 100644
index 00000000000..b2e0e82bad0
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class AppointmentTypeServiceItem(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
index c3242284674..325c2094fbf 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
@@ -121,6 +121,7 @@ class ClinicalProcedure(Document):
stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.to_warehouse = self.warehouse
+ stock_entry.company = self.company
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
for item in self.items:
if item.qty > item.actual_qty:
diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
index 4ee5f6bad39..fb72073a07f 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
+ # -*- coding: utf-8 -*-
# Copyright (c) 2017, ESS LLP and Contributors
# See license.txt
from __future__ import unicode_literals
@@ -60,6 +60,7 @@ def create_procedure(procedure_template, patient, practitioner):
procedure.practitioner = practitioner
procedure.consume_stock = procedure_template.allow_stock_consumption
procedure.items = procedure_template.items
- procedure.warehouse = frappe.db.get_single_value('Stock Settings', 'default_warehouse')
+ procedure.company = "_Test Company"
+ procedure.warehouse = "_Test Warehouse - _TC"
procedure.submit()
return procedure
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
index cb747f95ef8..8162f03f6dc 100644
--- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
+++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
@@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"label": "Out Patient Consulting Charge",
+ "mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency"
},
{
@@ -174,7 +175,8 @@
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
- "label": "Inpatient Visit Charge"
+ "label": "Inpatient Visit Charge",
+ "mandatory_depends_on": "inpatient_visit_charge_item"
},
{
"depends_on": "eval: !doc.__islocal",
@@ -280,7 +282,7 @@
],
"image_field": "image",
"links": [],
- "modified": "2020-04-06 13:44:24.759623",
+ "modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Practitioner",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 3d5073b13e7..0354733dfb7 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -24,11 +24,13 @@ frappe.ui.form.on('Patient Appointment', {
});
frm.set_query('practitioner', function() {
- return {
- filters: {
- 'department': frm.doc.department
- }
- };
+ if (frm.doc.department) {
+ return {
+ filters: {
+ 'department': frm.doc.department
+ }
+ };
+ }
});
frm.set_query('service_unit', function() {
@@ -140,6 +142,20 @@ frappe.ui.form.on('Patient Appointment', {
patient: function(frm) {
if (frm.doc.patient) {
frm.trigger('toggle_payment_fields');
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Patient',
+ name: frm.doc.patient
+ },
+ callback: function (data) {
+ let age = null;
+ if (data.message.dob) {
+ age = calculate_age(data.message.dob);
+ }
+ frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age);
+ }
+ });
} else {
frm.set_value('patient_name', '');
frm.set_value('patient_sex', '');
@@ -148,6 +164,37 @@ frappe.ui.form.on('Patient Appointment', {
}
},
+ practitioner: function(frm) {
+ if (frm.doc.practitioner ) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ appointment_type: function(frm) {
+ if (frm.doc.appointment_type) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ set_payment_details: function(frm) {
+ frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => {
+ if (val) {
+ frappe.call({
+ method: 'erpnext.healthcare.utils.get_service_item_and_practitioner_charge',
+ args: {
+ doc: frm.doc
+ },
+ callback: function(data) {
+ if (data.message) {
+ frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.practitioner_charge);
+ frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.service_item);
+ }
+ }
+ });
+ }
+ });
+ },
+
therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter');
},
@@ -190,14 +237,18 @@ frappe.ui.form.on('Patient Appointment', {
// show payment fields as non-mandatory
frm.toggle_display('mode_of_payment', 0);
frm.toggle_display('paid_amount', 0);
+ frm.toggle_display('billing_item', 0);
frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0);
+ frm.toggle_reqd('billing_item', 0);
} else {
// if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
frm.toggle_display('paid_amount', data.message ? 1 : 0);
+ frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
+ frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
});
@@ -540,57 +591,6 @@ let update_status = function(frm, status){
);
};
-frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) {
- if (frm.doc.practitioner) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Healthcare Practitioner',
- name: frm.doc.practitioner
- },
- callback: function (data) {
- frappe.model.set_value(frm.doctype, frm.docname, 'department', data.message.department);
- frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.op_consulting_charge);
- frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.op_consulting_charge_item);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'patient', function(frm) {
- if (frm.doc.patient) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Patient',
- name: frm.doc.patient
- },
- callback: function (data) {
- let age = null;
- if (data.message.dob) {
- age = calculate_age(data.message.dob);
- }
- frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'appointment_type', function(frm) {
- if (frm.doc.appointment_type) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Appointment Type',
- name: frm.doc.appointment_type
- },
- callback: function(data) {
- frappe.model.set_value(frm.doctype,frm.docname, 'duration',data.message.default_duration);
- }
- });
- }
-});
-
let calculate_age = function(birth) {
let ageMS = Date.parse(Date()) - Date.parse(birth);
let age = new Date();
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index 35600e48092..83c92af36ac 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -19,19 +19,19 @@
"inpatient_record",
"column_break_1",
"company",
+ "practitioner",
+ "practitioner_name",
+ "department",
"service_unit",
+ "section_break_12",
+ "appointment_type",
+ "duration",
"procedure_template",
"get_procedure_from_encounter",
"procedure_prescription",
"therapy_plan",
"therapy_type",
"get_prescribed_therapies",
- "practitioner",
- "practitioner_name",
- "department",
- "section_break_12",
- "appointment_type",
- "duration",
"column_break_17",
"appointment_date",
"appointment_time",
@@ -79,6 +79,7 @@
"set_only_once": 1
},
{
+ "fetch_from": "appointment_type.default_duration",
"fieldname": "duration",
"fieldtype": "Int",
"in_filter": 1,
@@ -144,7 +145,6 @@
"in_standard_filter": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
- "read_only": 1,
"reqd": 1,
"search_index": 1,
"set_only_once": 1
@@ -158,7 +158,6 @@
"in_standard_filter": 1,
"label": "Department",
"options": "Medical Department",
- "read_only": 1,
"search_index": 1,
"set_only_once": 1
},
@@ -227,12 +226,14 @@
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
- "options": "Mode of Payment"
+ "options": "Mode of Payment",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
- "label": "Paid Amount"
+ "label": "Paid Amount",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "column_break_2",
@@ -302,7 +303,8 @@
"fieldname": "therapy_plan",
"fieldtype": "Link",
"label": "Therapy Plan",
- "options": "Therapy Plan"
+ "options": "Therapy Plan",
+ "set_only_once": 1
},
{
"fieldname": "ref_sales_invoice",
@@ -347,7 +349,7 @@
}
],
"links": [],
- "modified": "2020-12-16 13:16:58.578503",
+ "modified": "2021-02-08 13:13:15.116833",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index f2b94b8e9c9..1f76cd624cd 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -26,6 +26,7 @@ class PatientAppointment(Document):
def after_insert(self):
self.update_prescription_details()
+ self.set_payment_details()
invoice_appointment(self)
self.update_fee_validity()
send_confirmation_msg(self)
@@ -85,6 +86,13 @@ class PatientAppointment(Document):
def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")
+ def set_payment_details(self):
+ if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
+ details = get_service_item_and_practitioner_charge(self)
+ self.db_set('billing_item', details.get('service_item'))
+ if not self.paid_amount:
+ self.db_set('paid_amount', details.get('practitioner_charge'))
+
def validate_customer_created(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
if not frappe.db.get_value('Patient', self.patient, 'customer'):
@@ -148,31 +156,37 @@ def invoice_appointment(appointment_doc):
fee_validity = None
if automate_invoicing and not appointment_invoiced and not fee_validity:
- sales_invoice = frappe.new_doc('Sales Invoice')
- sales_invoice.patient = appointment_doc.patient
- sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
- sales_invoice.appointment = appointment_doc.name
- sales_invoice.due_date = getdate()
- sales_invoice.company = appointment_doc.company
- sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
+ create_sales_invoice(appointment_doc)
- item = sales_invoice.append('items', {})
- item = get_appointment_item(appointment_doc, item)
- # Add payments if payment details are supplied else proceed to create invoice as Unpaid
- if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
- sales_invoice.is_pos = 1
- payment = sales_invoice.append('payments', {})
- payment.mode_of_payment = appointment_doc.mode_of_payment
- payment.amount = appointment_doc.paid_amount
+def create_sales_invoice(appointment_doc):
+ sales_invoice = frappe.new_doc('Sales Invoice')
+ sales_invoice.patient = appointment_doc.patient
+ sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
+ sales_invoice.appointment = appointment_doc.name
+ sales_invoice.due_date = getdate()
+ sales_invoice.company = appointment_doc.company
+ sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
- sales_invoice.set_missing_values(for_validate=True)
- sales_invoice.flags.ignore_mandatory = True
- sales_invoice.save(ignore_permissions=True)
- sales_invoice.submit()
- frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
+ item = sales_invoice.append('items', {})
+ item = get_appointment_item(appointment_doc, item)
+
+ # Add payments if payment details are supplied else proceed to create invoice as Unpaid
+ if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
+ sales_invoice.is_pos = 1
+ payment = sales_invoice.append('payments', {})
+ payment.mode_of_payment = appointment_doc.mode_of_payment
+ payment.amount = appointment_doc.paid_amount
+
+ sales_invoice.set_missing_values(for_validate=True)
+ sales_invoice.flags.ignore_mandatory = True
+ sales_invoice.save(ignore_permissions=True)
+ sales_invoice.submit()
+ frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
+ frappe.db.set_value('Patient Appointment', appointment_doc.name, {
+ 'invoiced': 1,
+ 'ref_sales_invoice': sales_invoice.name
+ })
def check_is_new_patient(patient, name=None):
@@ -187,13 +201,14 @@ def check_is_new_patient(patient, name=None):
def get_appointment_item(appointment_doc, item):
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc)
- item.item_code = service_item
+ details = get_service_item_and_practitioner_charge(appointment_doc)
+ charge = appointment_doc.paid_amount or details.get('practitioner_charge')
+ item.item_code = details.get('service_item')
item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner)
item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company)
item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center')
- item.rate = practitioner_charge
- item.amount = practitioner_charge
+ item.rate = charge
+ item.amount = charge
item.qty = 1
item.reference_dt = 'Patient Appointment'
item.reference_dn = appointment_doc.name
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index f7ec6f58fc5..2bb8a53c454 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -32,7 +32,8 @@ class TestPatientAppointment(unittest.TestCase):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
- self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1)
+ appointment.reload()
+ self.assertEqual(appointment.invoiced, 1)
encounter = make_encounter(appointment.name)
self.assertTrue(encounter)
self.assertEqual(encounter.company, appointment.company)
@@ -41,7 +42,7 @@ class TestPatientAppointment(unittest.TestCase):
# invoiced flag mapped from appointment
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
- def test_invoicing(self):
+ def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
@@ -57,6 +58,50 @@ class TestPatientAppointment(unittest.TestCase):
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+ def test_auto_invoicing_based_on_department(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ appointment_type = create_appointment_type()
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, 'HLC-SI-001')
+ self.assertEqual(appointment.paid_amount, 200)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+ self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+
+ def test_auto_invoicing_according_to_appointment_type_charge(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+
+ item = create_healthcare_service_items()
+ items = [{
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 300
+ }]
+ appointment_type = create_appointment_type(args={
+ 'name': 'Generic Appointment Type charge',
+ 'items': items
+ })
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name)
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, item)
+ self.assertEqual(appointment.paid_amount, 300)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
@@ -178,14 +223,15 @@ def create_encounter(appointment):
encounter.submit()
return encounter
-def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1):
+def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
+ service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
appointment = frappe.new_doc('Patient Appointment')
appointment.patient = patient
appointment.practitioner = practitioner
- appointment.department = '_Test Medical Department'
+ appointment.department = department or '_Test Medical Department'
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
@@ -193,7 +239,8 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.service_unit = service_unit
if invoice:
appointment.mode_of_payment = 'Cash'
- appointment.paid_amount = 500
+ if appointment_type:
+ appointment.appointment_type = appointment_type
if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name')
if save:
@@ -223,4 +270,29 @@ def create_clinical_procedure_template():
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
- return template
\ No newline at end of file
+ return template
+
+def create_appointment_type(args=None):
+ if not args:
+ args = frappe.local.form_dict
+
+ name = args.get('name') or 'Test Appointment Type wise Charge'
+
+ if frappe.db.exists('Appointment Type', name):
+ return frappe.get_doc('Appointment Type', name)
+
+ else:
+ item = create_healthcare_service_items()
+ items = [{
+ 'medical_department': '_Test Medical Department',
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 200
+ }]
+ return frappe.get_doc({
+ 'doctype': 'Appointment Type',
+ 'appointment_type': args.get('name') or 'Test Appointment Type wise Charge',
+ 'default_duration': args.get('default_duration') or 20,
+ 'color': args.get('color') or '#7575ff',
+ 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
+ 'items': args.get('items') or items
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index d4027dff4eb..d3d22c80b67 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -5,9 +5,11 @@
from __future__ import unicode_literals
import math
import frappe
+import json
from frappe import _
from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded
+from six import string_types
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple
@@ -64,7 +66,9 @@ def get_appointments_to_invoice(patient, company):
income_account = None
service_item = None
if appointment.practitioner:
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment)
+ details = get_service_item_and_practitioner_charge(appointment)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(appointment.practitioner, appointment.company)
appointments_to_invoice.append({
'reference_type': 'Patient Appointment',
@@ -97,7 +101,9 @@ def get_encounters_to_invoice(patient, company):
frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
+ details = get_service_item_and_practitioner_charge(encounter)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(encounter.practitioner, encounter.company)
encounters_to_invoice.append({
@@ -173,7 +179,7 @@ def get_clinical_procedures_to_invoice(patient, company):
if procedure.invoice_separately_as_consumables and procedure.consume_stock \
and procedure.status == 'Completed' and not procedure.consumption_invoiced:
- service_item = get_healthcare_service_item('clinical_procedure_consumable_item')
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
if not service_item:
msg = _('Please Configure Clinical Procedure Consumable Item in ')
msg += '''Healthcare Settings'''
@@ -304,24 +310,50 @@ def get_therapy_sessions_to_invoice(patient, company):
return therapy_sessions_to_invoice
-
+@frappe.whitelist()
def get_service_item_and_practitioner_charge(doc):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ service_item = None
+ practitioner_charge = None
+ department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department
+
is_inpatient = doc.inpatient_record
- if is_inpatient:
- service_item = get_practitioner_service_item(doc.practitioner, 'inpatient_visit_charge_item')
+
+ if doc.get('appointment_type'):
+ service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient)
+
+ if not service_item and not practitioner_charge:
+ service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient)
if not service_item:
- service_item = get_healthcare_service_item('inpatient_visit_charge_item')
- else:
- service_item = get_practitioner_service_item(doc.practitioner, 'op_consulting_charge_item')
- if not service_item:
- service_item = get_healthcare_service_item('op_consulting_charge_item')
+ service_item = get_healthcare_service_item(is_inpatient)
+
if not service_item:
throw_config_service_item(is_inpatient)
- practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
if not practitioner_charge:
throw_config_practitioner_charge(is_inpatient, doc.practitioner)
+ return {'service_item': service_item, 'practitioner_charge': practitioner_charge}
+
+
+def get_appointment_type_service_item(appointment_type, department, is_inpatient):
+ from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department
+
+ item_list = get_service_item_based_on_department(appointment_type, department)
+ service_item = None
+ practitioner_charge = None
+
+ if item_list:
+ if is_inpatient:
+ service_item = item_list.get('inpatient_visit_charge_item')
+ practitioner_charge = item_list.get('inpatient_visit_charge')
+ else:
+ service_item = item_list.get('op_consulting_charge_item')
+ practitioner_charge = item_list.get('op_consulting_charge')
+
return service_item, practitioner_charge
@@ -345,12 +377,27 @@ def throw_config_practitioner_charge(is_inpatient, practitioner):
frappe.throw(msg, title=_('Missing Configuration'))
-def get_practitioner_service_item(practitioner, service_item_field):
- return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field)
+def get_practitioner_service_item(practitioner, is_inpatient):
+ service_item = None
+ practitioner_charge = None
+
+ if is_inpatient:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge'])
+ else:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge'])
+
+ return service_item, practitioner_charge
-def get_healthcare_service_item(service_item_field):
- return frappe.db.get_single_value('Healthcare Settings', service_item_field)
+def get_healthcare_service_item(is_inpatient):
+ service_item = None
+
+ if is_inpatient:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item')
+ else:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item')
+
+ return service_item
def get_practitioner_charge(practitioner, is_inpatient):
@@ -381,7 +428,8 @@ def set_invoiced(item, method, ref_invoice=None):
invoiced = True
if item.reference_dt == 'Clinical Procedure':
- if get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
+ if service_item == item.item_code:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced)
else:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced)
@@ -403,7 +451,8 @@ def set_invoiced(item, method, ref_invoice=None):
def validate_invoiced_on_submit(item):
- if item.reference_dt == 'Clinical Procedure' and get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ if item.reference_dt == 'Clinical Procedure' and \
+ frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced')
else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 109d9216e7d..347da6b563d 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -399,6 +399,7 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header',
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data',
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
+ 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
index 6324b049272..9f667a68356 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
@@ -4,11 +4,11 @@
{{ __("Leave Type") }}
- {{ __("Total Allocated Leaves") }}
- {{ __("Expired Leaves") }}
- {{ __("Used Leaves") }}
- {{ __("Pending Leaves") }}
- {{ __("Available Leaves") }}
+ {{ __("Total Allocated Leave") }}
+ {{ __("Expired Leave") }}
+ {{ __("Used Leave") }}
+ {{ __("Pending Leave") }}
+ {{ __("Available Leave") }}
No Leaves have been allocated.
-{% endif %} \ No newline at end of file +No Leave has been allocated.
+{% endif %} diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 1b923581841..06f9160363c 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -40,17 +40,17 @@ def get_columns(): 'fieldname': 'opening_balance', 'width': 130, }, { - 'label': _('Leaves Allocated'), + 'label': _('Leave Allocated'), 'fieldtype': 'float', 'fieldname': 'leaves_allocated', 'width': 130, }, { - 'label': _('Leaves Taken'), + 'label': _('Leave Taken'), 'fieldtype': 'float', 'fieldname': 'leaves_taken', 'width': 130, }, { - 'label': _('Leaves Expired'), + 'label': _('Leave Expired'), 'fieldtype': 'float', 'fieldname': 'leaves_expired', 'width': 130, diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py index 7d7992d40ae..7978350adf8 100644 --- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py +++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py @@ -246,7 +246,5 @@ def get_per_day_interest(principal_amount, rate_of_interest, posting_date=None): if not posting_date: posting_date = getdate() - precision = cint(frappe.db.get_default("currency_precision")) or 2 - - return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision) + return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)) diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py index adc8013c686..8b6ea869a3d 100644 --- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py +++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py @@ -76,7 +76,7 @@ def get_company_wise_loan_security_details(filters, loan_security_details): if qty: security_wise_map[key[1]]['applicant_count'] += 1 - total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price']) + total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0)) return security_wise_map, total_portfolio_value diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index ec28eb7795c..662a06b1ee2 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -267,6 +267,17 @@ class JobCard(Document): fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id}) + def set_transferred_qty_in_job_card(self, ste_doc): + for row in ste_doc.items: + if not row.job_card_item: continue + + qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se + WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and + se.purpose = 'Material Transfer for Manufacture' + """, (row.job_card_item))[0][0] + + frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty)) + def set_transferred_qty(self, update_status=False): if not self.items: self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 @@ -279,7 +290,8 @@ class JobCard(Document): self.transferred_qty = frappe.db.get_value('Stock Entry', { 'job_card': self.name, 'work_order': self.work_order, - 'docstatus': 1 + 'docstatus': 1, + 'purpose': 'Material Transfer for Manufacture' }, 'sum(fg_completed_qty)') or 0 self.db_set("transferred_qty", self.transferred_qty) @@ -420,6 +432,7 @@ def make_stock_entry(source_name, target_doc=None): target.purpose = "Material Transfer for Manufacture" target.from_bom = 1 target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) + target.set_transfer_qty() target.calculate_rate_and_amount() target.set_missing_values() target.set_stock_entry_type() @@ -437,9 +450,10 @@ def make_stock_entry(source_name, target_doc=None): "field_map": { "source_warehouse": "s_warehouse", "required_qty": "qty", - "uom": "stock_uom" + "name": "job_card_item" }, "postprocess": update_item, + "condition": lambda doc: doc.required_qty > 0 } }, target_doc, set_missing_values) diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json index bc9fe108ca6..100ef4ca3a3 100644 --- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json +++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json @@ -1,363 +1,120 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-07-09 17:20:44.737289", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-07-09 17:20:44.737289", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "source_warehouse", + "uom", + "item_group", + "column_break_3", + "stock_uom", + "item_name", + "description", + "qty_section", + "required_qty", + "column_break_9", + "transferred_qty", + "allow_alternative_item" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_code", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Item Code", - "length": 0, - "no_copy": 0, - "options": "Item", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "source_warehouse", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Source Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "source_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "Source Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "uom", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "UOM", - "length": 0, - "no_copy": 0, - "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_3", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Item Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "description", + "fieldtype": "Text", + "label": "Description", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "qty_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "qty_section", + "fieldtype": "Section Break", + "label": "Qty" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "required_qty", - "fieldtype": "Float", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Required Qty", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "allow_alternative_item", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Allow Alternative Item", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "default": "0", + "fieldname": "allow_alternative_item", + "fieldtype": "Check", + "label": "Allow Alternative Item" + }, + { + "fetch_from": "item_code.item_group", + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group", + "read_only": 1 + }, + { + "fetch_from": "item_code.stock_uom", + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock UOM", + "options": "UOM" + }, + { + "fieldname": "transferred_qty", + "fieldtype": "Float", + "label": "Transferred Qty", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-08-28 15:23:48.099459", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Job Card Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-02-11 13:50:13.804108", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index 06a8e1987d3..00e8c5418a0 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -94,11 +94,11 @@ class TestWorkOrder(unittest.TestCase): wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, source_warehouse=warehouse, skip_transfer=1) - bin1_on_submit = get_bin(item, warehouse) + reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production) # reserved qty for production is updated - self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, - cint(bin1_on_submit.reserved_qty_for_production)) + self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission) + test_stock_entry.make_stock_entry(item_code="_Test Item", target=warehouse, qty=100, basic_rate=100) @@ -109,9 +109,9 @@ class TestWorkOrder(unittest.TestCase): s.submit() bin1_at_completion = get_bin(item, warehouse) - + self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), - cint(bin1_on_submit.reserved_qty_for_production) - 1) + reserved_qty_on_submission - 1) def test_production_item(self): wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 3b7c6ab48eb..e5ee551c118 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -752,3 +752,5 @@ erpnext.patches.v13_0.set_company_in_leave_ledger_entry erpnext.patches.v13_0.convert_qi_parameter_to_link_field erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 +erpnext.patches.v12_0.add_state_code_for_ladakh +erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl diff --git a/erpnext/patches/v12_0/add_state_code_for_ladakh.py b/erpnext/patches/v12_0/add_state_code_for_ladakh.py new file mode 100644 index 00000000000..d41101cc46a --- /dev/null +++ b/erpnext/patches/v12_0/add_state_code_for_ladakh.py @@ -0,0 +1,16 @@ +import frappe +from erpnext.regional.india import states + +def execute(): + + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + custom_fields = ['Address-gst_state', 'Tax Category-gst_state'] + + # Update options in gst_state custom fields + for field in custom_fields: + gst_state_field = frappe.get_doc('Custom Field', field) + gst_state_field.options = '\n'.join(states) + gst_state_field.save() diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py new file mode 100644 index 00000000000..f60e0d3036c --- /dev/null +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -0,0 +1,27 @@ +import frappe +from frappe import _ +from erpnext.stock.stock_ledger import update_entries_after +from erpnext.accounts.utils import update_gl_entries_after + +def execute(): + data = frappe.db.sql(''' SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time + from `tabStock Ledger Entry` where creation > '2020-12-26 12:58:55.903836' and is_cancelled = 0 + order by timestamp(posting_date, posting_time) asc, creation asc''', as_dict=1) + + for index, d in enumerate(data): + update_entries_after({ + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": d.posting_date, + "posting_time": d.posting_time, + "voucher_type": d.voucher_type, + "voucher_no": d.voucher_no, + "sle_id": d.name + }, allow_negative_stock=True) + + frappe.db.auto_commit_on_many_writes = 1 + + for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + update_gl_entries_after('2020-12-25', '01:58:55', company=row.name) + + frappe.db.auto_commit_on_many_writes = 0 \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 2d3bc57900d..60aff02b38d 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -1103,10 +1103,10 @@ class SalarySlip(TransactionBase): self.calculate_total_for_salary_slip_based_on_timesheet() else: self.total_deduction = 0.0 - if self.earnings: + if hasattr(self, "earnings"): for earning in self.earnings: self.gross_pay += flt(earning.amount, earning.precision("amount")) - if self.deductions: + if hasattr(self, "deductions"): for deduction in self.deductions: self.total_deduction += flt(deduction.amount, deduction.precision("amount")) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment) diff --git a/erpnext/public/images/erpnext-logo.png b/erpnext/public/images/erpnext-logo.png new file mode 100644 index 00000000000..3090727d8ff Binary files /dev/null and b/erpnext/public/images/erpnext-logo.png differ diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 416495ceac7..d81321b2916 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -2,7 +2,9 @@ // License: GNU General Public License v3. See license.txt erpnext.taxes_and_totals = erpnext.payments.extend({ - setup: function() {}, + setup: function() { + this.fetch_round_off_accounts(); + }, apply_pricing_rule_on_item: function(item) { let effective_item_rate = item.price_list_rate; @@ -152,6 +154,22 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }); }, + fetch_round_off_accounts: function() { + let me = this; + frappe.flags.round_off_applicable_accounts = []; + + return frappe.call({ + "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", + "args": { + "company": me.frm.doc.company, + "account_list": frappe.flags.round_off_applicable_accounts + }, + callback: function(r) { + frappe.flags.round_off_applicable_accounts.push(...r.message); + } + }); + }, + determine_exclusive_rate: function() { var me = this; @@ -372,11 +390,21 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if (tax.charge_type == "On Item Quantity") { current_tax_amount = tax_rate * item.qty; } + + current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount); this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount); return current_tax_amount; }, + get_final_tax_amount: function(tax, current_tax_amount) { + if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) { + current_tax_amount = Math.round(current_tax_amount); + } + + return current_tax_amount; + }, + set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) { // store tax breakup for each item let tax_detail = tax.item_wise_tax_detail; diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js index 2623c3c1a73..d49a8138fb5 100644 --- a/erpnext/public/js/utils/serial_no_batch_selector.js +++ b/erpnext/public/js/utils/serial_no_batch_selector.js @@ -140,6 +140,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ () => me.update_batch_serial_no_items(), () => { refresh_field("items"); + refresh_field("packed_items"); if (me.callback) { return me.callback(me.item); } @@ -154,7 +155,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ if (this.item.serial_no) { this.dialog.fields_dict.serial_no.set_value(this.item.serial_no); } - + if (this.has_batch && !this.has_serial_no && d.batch_no) { this.frm.doc.items.forEach(data => { if(data.item_code == d.item_code) { @@ -231,7 +232,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ this.map_row_values(row, batch, 'batch_no', 'selected_qty', this.values.warehouse); }); - } + } }, update_serial_no_item() { @@ -250,7 +251,7 @@ erpnext.SerialNoBatchSelector = Class.extend({ filters: { 'name': ["in", selected_serial_nos]}, fields: ["batch_no", "name"] }).then((data) => { - // data = [{batch_no: 'batch-1', name: "SR-001"}, + // data = [{batch_no: 'batch-1', name: "SR-001"}, // {batch_no: 'batch-2', name: "SR-003"}, {batch_no: 'batch-2', name: "SR-004"}] const batch_serial_map = data.reduce((acc, d) => { if (!acc[d['batch_no']]) acc[d['batch_no']] = []; @@ -298,6 +299,8 @@ erpnext.SerialNoBatchSelector = Class.extend({ } else { row.warehouse = values.warehouse || warehouse; } + + this.frm.dirty(); }, update_total_qty: function() { diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less index 8cb7a9c1ce5..29deada8a41 100644 --- a/erpnext/public/less/hub.less +++ b/erpnext/public/less/hub.less @@ -32,7 +32,12 @@ body[data-route*="marketplace"] { } .hub-image-loading, .hub-image-broken { - .img-background(); + content: " "; + position: absolute; + left: 0; + height: 100%; + width: 100%; + background-color: var(--bg-light-gray); display: flex; align-items: center; justify-content: center; diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.json b/erpnext/regional/doctype/gst_settings/gst_settings.json index 98c33ad33bb..95b930c4c86 100644 --- a/erpnext/regional/doctype/gst_settings/gst_settings.json +++ b/erpnext/regional/doctype/gst_settings/gst_settings.json @@ -1,222 +1,86 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-06-27 15:09:01.318003", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-06-27 15:09:01.318003", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "gst_summary", + "column_break_2", + "round_off_gst_values", + "gstin_email_sent_on", + "section_break_4", + "gst_accounts", + "b2c_limit" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gst_summary", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "GST Summary", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gst_summary", + "fieldtype": "HTML", + "label": "GST Summary", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gstin_email_sent_on", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "GSTIN Email Sent On", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gstin_email_sent_on", + "fieldtype": "Date", + "label": "GSTIN Email Sent On", + "read_only": 1, + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_4", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "section_break_4", + "fieldtype": "Section Break", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "gst_accounts", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "GST Accounts", - "length": 0, - "no_copy": 0, - "options": "GST Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "gst_accounts", + "fieldtype": "Table", + "label": "GST Accounts", + "options": "GST Account", + "show_days": 1, + "show_seconds": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "250000", - "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.", - "fieldname": "b2c_limit", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "B2C Limit", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "default": "250000", + "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.", + "fieldname": "b2c_limit", + "fieldtype": "Data", + "in_list_view": 1, + "label": "B2C Limit", + "reqd": 1, + "show_days": 1, + "show_seconds": 1 + }, + { + "default": "0", + "description": "Enabling this option will round off individual GST components in all the Invoices", + "fieldname": "round_off_gst_values", + "fieldtype": "Check", + "label": "Round Off GST Values", + "show_days": 1, + "show_seconds": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2018-02-14 08:14:15.375181", - "modified_by": "Administrator", - "module": "Regional", - "name": "GST Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-01-28 17:19:47.969260", + "modified_by": "Administrator", + "module": "Regional", + "name": "GST Settings", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 + } \ No newline at end of file diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index 8174da20cb7..023b4ed22bc 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -14,8 +14,20 @@ import json test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] class TestGSTR3BReport(unittest.TestCase): - def test_gstr_3b_report(self): + def setUp(self): + frappe.set_user("Administrator") + frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'") + frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'") + frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'") + + make_company() + make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000}) + set_account_heads() + make_customers() + make_suppliers() + + def test_gstr_3b_report(self): month_number_mapping = { 1: "January", 2: "February", @@ -31,17 +43,6 @@ class TestGSTR3BReport(unittest.TestCase): 12: "December" } - frappe.set_user("Administrator") - - frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'") - frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'") - frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'") - - make_company() - make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000}) - set_account_heads() - make_customers() - make_suppliers() make_sales_invoice() create_purchase_invoices() @@ -67,6 +68,42 @@ class TestGSTR3BReport(unittest.TestCase): self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50) self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50) + def test_gst_rounding(self): + gst_settings = frappe.get_doc('GST Settings') + gst_settings.round_off_gst_values = 1 + gst_settings.save() + + current_country = frappe.flags.country + frappe.flags.country = 'India' + + si = create_sales_invoice(company="_Test Company GST", + customer = '_Test GST Customer', + currency = 'INR', + warehouse = 'Finished Goods - _GST', + debit_to = 'Debtors - _GST', + income_account = 'Sales - _GST', + expense_account = 'Cost of Goods Sold - _GST', + cost_center = 'Main - _GST', + rate=216, + do_not_save=1 + ) + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": "IGST - _GST", + "cost_center": "Main - _GST", + "description": "IGST @ 18.0", + "rate": 18 + }) + + si.save() + # Check for 39 instead of 38.88 + self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 39) + + frappe.flags.country = current_country + gst_settings.round_off_gst_values = 1 + gst_settings.save() + def make_sales_invoice(): si = create_sales_invoice(company="_Test Company GST", customer = '_Test GST Customer', @@ -145,7 +182,6 @@ def make_sales_invoice(): si3.submit() def create_purchase_invoices(): - pi = make_purchase_invoice( company="_Test Company GST", supplier = '_Test Registered Supplier', @@ -193,7 +229,6 @@ def create_purchase_invoices(): pi1.submit() def make_suppliers(): - if not frappe.db.exists("Supplier", "_Test Registered Supplier"): frappe.get_doc({ "supplier_group": "_Test Supplier Group", @@ -257,7 +292,6 @@ def make_suppliers(): address.save() def make_customers(): - if not frappe.db.exists("Customer", "_Test GST Customer"): frappe.get_doc({ "customer_group": "_Test Customer Group", @@ -354,9 +388,9 @@ def make_customers(): address.save() def make_company(): - if frappe.db.exists("Company", "_Test Company GST"): return + company = frappe.new_doc("Company") company.company_name = "_Test Company GST" company.abbr = "_GST" @@ -388,7 +422,6 @@ def make_company(): address.save() def set_account_heads(): - gst_settings = frappe.get_doc("GST Settings") gst_account = frappe.get_all( diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py index e8a8ed87505..ad60db05595 100644 --- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -5,12 +5,16 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import getdate +from frappe.utils import getdate, get_link_to_form from frappe.model.document import Document from erpnext.accounts.utils import get_fiscal_year class LowerDeductionCertificate(Document): def validate(self): + self.validate_dates() + self.validate_supplier_against_section_code() + + def validate_dates(self): if getdate(self.valid_upto) < getdate(self.valid_from): frappe.throw(_("Valid Upto date cannot be before Valid From date")) @@ -24,3 +28,20 @@ class LowerDeductionCertificate(Document): <= fiscal_year.year_end_date): frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + def validate_supplier_against_section_code(self): + duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', {'supplier': self.supplier, 'section_code': self.section_code}, ['name', 'valid_from', 'valid_upto'], as_dict=True) + if duplicate_certificate and self.are_dates_overlapping(duplicate_certificate): + certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate.name) + frappe.throw(_("There is already a valid Lower Deduction Certificate {0} for Supplier {1} against Section Code {2} for this time period.") + .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.section_code))) + + def are_dates_overlapping(self,duplicate_certificate): + valid_from = duplicate_certificate.valid_from + valid_upto = duplicate_certificate.valid_upto + if valid_from <= getdate(self.valid_from) <= valid_upto: + return True + elif valid_from <= getdate(self.valid_upto) <= valid_upto: + return True + elif getdate(self.valid_from) <= valid_from and valid_upto <= getdate(self.valid_upto): + return True + return False \ No newline at end of file diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index d6221a80aa2..378b735e078 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -20,6 +20,7 @@ states = [ 'Jharkhand', 'Karnataka', 'Kerala', + 'Ladakh', 'Lakshadweep Islands', 'Madhya Pradesh', 'Maharashtra', @@ -59,6 +60,7 @@ state_numbers = { "Jharkhand": "20", "Karnataka": "29", "Kerala": "32", + "Ladakh": "38", "Lakshadweep Islands": "31", "Madhya Pradesh": "23", "Maharashtra": "27", @@ -80,4 +82,4 @@ state_numbers = { "West Bengal": "19", } -number_state_mapping = {v: k for k, v in iteritems(state_numbers)} \ No newline at end of file +number_state_mapping = {v: k for k, v in iteritems(state_numbers)} diff --git a/erpnext/regional/india/gst_state_code_data.json b/erpnext/regional/india/gst_state_code_data.json index ff88e0f9d6c..8481c279728 100644 --- a/erpnext/regional/india/gst_state_code_data.json +++ b/erpnext/regional/india/gst_state_code_data.json @@ -168,5 +168,10 @@ "state_number": "37", "state_code": "AD", "state_name": "Andhra Pradesh (New)" + }, + { + "state_number": "38", + "state_code": "LA", + "state_name": "Ladakh" } ] diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index e89885f3805..09bafdd7f23 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -772,3 +772,24 @@ def make_regional_gl_entries(gl_entries, doc): ) return gl_entries + +@frappe.whitelist() +def get_regional_round_off_accounts(company, account_list): + country = frappe.get_cached_value('Company', company, 'country') + + if country != 'India': + return + + if isinstance(account_list, string_types): + account_list = json.loads(account_list) + + if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'): + return + + gst_accounts = get_gst_accounts(company) + gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ + + gst_accounts.get('igst_account') + + account_list.extend(gst_account_list) + + return account_list \ No newline at end of file diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 96dc3f728d9..09b04ff367f 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -236,6 +236,7 @@ class Gstr1Report(object): self.cgst_sgst_invoices = [] unidentified_gst_accounts = [] + unidentified_gst_accounts_invoice = [] for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: if account in self.gst_accounts.cess_account: self.invoice_cess.setdefault(parent, tax_amount) @@ -251,6 +252,7 @@ class Gstr1Report(object): if not (cgst_or_sgst or account in self.gst_accounts.igst_account): if "gst" in account.lower() and account not in unidentified_gst_accounts: unidentified_gst_accounts.append(account) + unidentified_gst_accounts_invoice.append(parent) continue for item_code, tax_amounts in item_wise_tax_detail.items(): @@ -273,7 +275,7 @@ class Gstr1Report(object): # Build itemised tax for export invoices where tax table is blank for invoice, items in iteritems(self.invoice_items): - if invoice not in self.items_based_on_tax_rate \ + if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 557c7151d96..3c0652eb674 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -34,9 +34,8 @@ "companies", "currency_and_price_list", "default_currency", - "default_price_list", "column_break_14", - "language", + "default_price_list", "address_contacts", "address_html", "website", @@ -59,6 +58,7 @@ "column_break_45", "market_segment", "industry", + "language", "is_frozen", "column_break_38", "loyalty_program", @@ -485,7 +485,7 @@ "idx": 363, "image_field": "image", "links": [], - "modified": "2020-03-17 11:03:42.706907", + "modified": "2021-01-06 19:35:25.418017", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index e4923773859..e3b41e66fbc 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -514,7 +514,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( make_delivery_note: function() { frappe.model.open_mapped_doc({ method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", - frm: me.frm + frm: this.frm }) }, diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 1516dd6d954..e56129170c1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -180,6 +180,7 @@ class SalesOrder(SellingController): update_coupon_code_count(self.coupon_code,'used') def on_cancel(self): + self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') super(SalesOrder, self).on_cancel() # Cannot cancel closed SO diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index cbfab8204f0..52a0174798e 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -17,6 +17,18 @@ from erpnext.selling.doctype.product_bundle.test_product_bundle import make_prod from erpnext.stock.doctype.item.test_item import make_item class TestSalesOrder(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order")) + + @classmethod + def tearDownClass(cls) -> None: + # reset config to previous state + frappe.db.set_value("Accounts Settings", "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting) + def tearDown(self): frappe.set_user("Administrator") @@ -1049,6 +1061,38 @@ class TestSalesOrder(unittest.TestCase): self.assertRaises(frappe.LinkExistsError, so_doc.cancel) + def test_cancel_sales_order_after_cancel_payment_entry(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + # make a sales order + so = make_sales_order() + + # disable unlinking of payment entry + frappe.db.set_value("Accounts Settings", "Accounts Settings", + "unlink_advance_payment_on_cancelation_of_order", 0) + + # create a payment entry against sales order + pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC") + pe.reference_no = "1" + pe.reference_date = nowdate() + pe.paid_from_account_currency = so.currency + pe.paid_to_account_currency = so.currency + pe.source_exchange_rate = 1 + pe.target_exchange_rate = 1 + pe.paid_amount = so.grand_total + pe.save(ignore_permissions=True) + pe.submit() + + # Cancel payment entry + po_doc = frappe.get_doc("Payment Entry", pe.name) + po_doc.cancel() + + # Cancel sales order + try: + so_doc = frappe.get_doc('Sales Order', so.name) + so_doc.cancel() + except Exception: + self.fail("Can not cancel sales order with linked cancelled payment entry") + def test_request_for_raw_materials(self): item = make_item("_Test Finished Item", {"is_stock_item": 1, "maintain_stock": 1, @@ -1207,4 +1251,4 @@ def make_sales_order_workflow(): )) workflow.insert(ignore_permissions=True) - return workflow \ No newline at end of file + return workflow diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 7a72fe31023..0df4c87f51f 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -28,7 +28,7 @@ def delete_company_transactions(company_name): "Party Account", "Employee", "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template", "POS Profile", "BOM", "Company", "Bank Account", "Item Tax Template", "Mode Of Payment", - "Item Default", "Customer", "Supplier"): + "Item Default", "Customer", "Supplier", "GST Account"): delete_for_doctype(doctype, company_name) # reset company values diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 3c9f84982bd..681d161edcd 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -462,6 +462,9 @@ def get_party(user=None): return customer def get_debtors_account(cart_settings): + if not cart_settings.payment_gateway_account: + frappe.throw(_("Payment Gateway Account not set"), _("Mandatory")) + payment_gateway_account_currency = \ frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json index 702583aead4..36917213022 100644 --- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json +++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json @@ -26,10 +26,10 @@ "quotation_series", "section_break_8", "enable_checkout", - "payment_success_url", - "column_break_11", "save_quotations_as_draft", - "payment_gateway_account" + "column_break_11", + "payment_gateway_account", + "payment_success_url" ], "fields": [ { @@ -143,10 +143,12 @@ }, { "default": "Orders", + "depends_on": "enable_checkout", "description": "After payment completion redirect user to selected page.", "fieldname": "payment_success_url", "fieldtype": "Select", "label": "Payment Success Url", + "mandatory_depends_on": "enable_checkout", "options": "\nOrders\nInvoices\nMy Account" }, { @@ -154,9 +156,11 @@ "fieldtype": "Column Break" }, { + "depends_on": "enable_checkout", "fieldname": "payment_gateway_account", "fieldtype": "Link", "label": "Payment Gateway Account", + "mandatory_depends_on": "enable_checkout", "options": "Payment Gateway Account" }, { @@ -186,7 +190,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2021-02-01 18:18:54.606535", + "modified": "2021-02-11 18:48:30.433058", "modified_by": "Administrator", "module": "Shopping Cart", "name": "Shopping Cart Settings", diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index 8d64efe41dd..9e240cc2b3c 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -64,10 +64,10 @@ def get_warehouse_account(warehouse, warehouse_account=None): if not account and warehouse.company: account = get_company_default_inventory_account(warehouse.company) - if not account and warehouse.company: + if not account and warehouse.company and not warehouse.is_group: frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}") .format(warehouse.name, warehouse.company)) return account def get_company_default_inventory_account(company): - return frappe.get_cached_value('Company', company, 'default_inventory_account') + return frappe.get_cached_value('Company', company, 'default_inventory_account') diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py index 97f85bafd95..cbd272df4b8 100644 --- a/erpnext/stock/doctype/batch/test_batch.py +++ b/erpnext/stock/doctype/batch/test_batch.py @@ -298,9 +298,9 @@ class TestBatch(unittest.TestCase): self.assertEqual(details.get('price_list_rate'), 400) def create_batch(item_code, rate, create_item_price_for_batch): - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", - warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, - expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) + pi = make_purchase_invoice(company="_Test Company", + warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1, + expense_account ="_Test Account Cost for Goods Sold - _TC", item_code=item_code) batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name}) diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py index 1088b4127d3..0514bd23942 100644 --- a/erpnext/stock/doctype/bin/bin.py +++ b/erpnext/stock/doctype/bin/bin.py @@ -16,8 +16,9 @@ class Bin(Document): def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False): '''Called from erpnext.stock.utils.update_bin''' self.update_qty(args) + if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": - from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle + from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle if not args.get("posting_date"): args["posting_date"] = nowdate() @@ -34,11 +35,13 @@ class Bin(Document): "posting_time": args.get("posting_time"), "voucher_type": args.get("voucher_type"), "voucher_no": args.get("voucher_no"), - "sle_id": args.name + "sle_id": args.name, + "creation": args.creation }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) - # Validate negative qty in future transactions - validate_negative_qty_in_future_sle(args) + # update qty in future ale and Validate negative qty + update_qty_in_future_sle(args, allow_negative_stock) + def update_qty(self, args): # update the stock values (for current quantities) @@ -51,7 +54,7 @@ class Bin(Document): self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty")) self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty")) self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty")) - + self.set_projected_qty() self.db_update() diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index 559f8be0dea..d39b22965e3 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -489,7 +489,10 @@ class TestDeliveryNote(unittest.TestCase): def test_closed_delivery_note(self): from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status - dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) + make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100) + + dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', + cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True) dn.submit() diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index 144101c67d7..984ae46c66c 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -148,7 +148,6 @@ class TestLandedCostVoucher(unittest.TestCase): def test_landed_cost_voucher_for_odd_numbers (self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) pr.items[0].cost_center = "Main - TCP1" for x in range(2): @@ -208,6 +207,10 @@ class TestLandedCostVoucher(unittest.TestCase): self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100) def test_multi_currency_lcv(self): + from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records, save_new_records + + save_new_records(test_records) + ## Create USD Shipping charges_account usd_shipping = create_account(account_name="Shipping Charges USD", parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index ca58ab28236..7741ee7f609 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -94,10 +94,15 @@ class TestPurchaseReceipt(unittest.TestCase): frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete() def test_purchase_receipt_no_gl_entry(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry + company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') - existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", - "warehouse": "_Test Warehouse - _TC"}, "stock_value") + existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", + "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"]) + + if existing_bin_qty < 0: + make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty)) pr = make_purchase_receipt() diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py index ba2c2c6f446..8436acbed25 100644 --- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py @@ -46,6 +46,9 @@ class RepostItemValuation(Document): def repost(doc): try: + if not frappe.db.exists("Repost Item Valuation", doc.name): + return + doc.set_status('In Progress') frappe.db.commit() @@ -64,7 +67,7 @@ def repost(doc): message += "