Merge branch 'develop' into loan_exposure_report_fix

This commit is contained in:
Deepesh Garg
2021-02-20 17:32:17 +05:30
committed by GitHub
67 changed files with 1270 additions and 836 deletions

View File

@@ -109,7 +109,7 @@ def get_region(company=None):
''' '''
if company or frappe.flags.company: if company or frappe.flags.company:
return frappe.get_cached_value('Company', return frappe.get_cached_value('Company',
company or frappe.flags.company, 'country') company or frappe.flags.company, 'country')
elif frappe.flags.country: elif frappe.flags.country:
return frappe.flags.country return frappe.flags.country
else: else:

View File

@@ -179,10 +179,18 @@ class POSInvoice(SalesInvoice):
if d.get("serial_no"): if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no) serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos: for sr in serial_nos:
serial_no_exists = frappe.db.exists("POS Invoice Item", { serial_no_exists = frappe.db.sql("""
"parent": self.return_against, SELECT name
"serial_no": ["like", d.get("serial_no")] 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: if not serial_no_exists:
bold_return_against = frappe.bold(self.return_against) bold_return_against = frappe.bold(self.return_against)
bold_serial_no = frappe.bold(sr) 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 {}") _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
.format(d.idx, bold_serial_no, bold_return_against) .format(d.idx, bold_serial_no, bold_return_against)
) )
def validate_non_stock_items(self): def validate_non_stock_items(self):
for d in self.get("items"): for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") 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: if not self.get('payments') and not for_validate:
update_multi_mode_option(self, profile) update_multi_mode_option(self, profile)
if self.is_return and not for_validate: if self.is_return and not for_validate:
add_return_modes(self, profile) add_return_modes(self, profile)

View File

@@ -198,6 +198,65 @@ class TestPOSInvoice(unittest.TestCase):
self.assertEqual(pos_return.get('payments')[0].amount, -500) self.assertEqual(pos_return.get('payments')[0].amount, -500)
self.assertEqual(pos_return.get('payments')[1].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): def test_pos_change_amount(self):
pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC", pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,

View File

@@ -87,6 +87,7 @@
"edit_references", "edit_references",
"sales_order", "sales_order",
"so_detail", "so_detail",
"pos_invoice_item",
"column_break_74", "column_break_74",
"delivery_note", "delivery_note",
"dn_detail", "dn_detail",
@@ -790,11 +791,20 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Project", "label": "Project",
"options": "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, "istable": 1,
"links": [], "links": [],
"modified": "2020-07-22 13:40:34.418346", "modified": "2021-01-04 17:34:49.924531",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Item", "name": "POS Invoice Item",

View File

@@ -29,7 +29,7 @@ class POSInvoiceMergeLog(Document):
for d in self.pos_invoices: for d in self.pos_invoices:
status, docstatus, is_return, return_against = frappe.db.get_value( status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
bold_pos_invoice = frappe.bold(d.pos_invoice) bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status) bold_status = frappe.bold(status)
if docstatus != 1: if docstatus != 1:
@@ -58,7 +58,7 @@ class POSInvoiceMergeLog(Document):
sales_invoice, credit_note = "", "" sales_invoice, credit_note = "", ""
if sales: if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales) sales_invoice = self.process_merging_into_sales_invoice(sales)
if returns: if returns:
credit_note = self.process_merging_into_credit_note(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): def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice() sales_invoice = self.get_new_sales_invoice()
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1 sales_invoice.is_consolidated = 1
@@ -98,19 +98,19 @@ class POSInvoiceMergeLog(Document):
self.consolidated_credit_note = credit_note.name self.consolidated_credit_note = credit_note.name
return credit_note.name return credit_note.name
def merge_pos_invoice_into(self, invoice, data): def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], [] items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0 loyalty_amount_sum, loyalty_points_sum = 0, 0
for doc in data: for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype }) map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
if doc.redeem_loyalty_points: if doc.redeem_loyalty_points:
invoice.loyalty_redemption_account = doc.loyalty_redemption_account invoice.loyalty_redemption_account = doc.loyalty_redemption_account
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
loyalty_points_sum += doc.loyalty_points loyalty_points_sum += doc.loyalty_points
loyalty_amount_sum += doc.loyalty_amount loyalty_amount_sum += doc.loyalty_amount
for item in doc.get('items'): for item in doc.get('items'):
found = False found = False
for i in items: for i in items:
@@ -118,12 +118,13 @@ class POSInvoiceMergeLog(Document):
i.uom == item.uom and i.net_rate == item.net_rate): i.uom == item.uom and i.net_rate == item.net_rate):
found = True found = True
i.qty = i.qty + item.qty i.qty = i.qty + item.qty
if not found: if not found:
item.rate = item.net_rate item.rate = item.net_rate
item.price_list_rate = 0 item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item) items.append(si_item)
for tax in doc.get('taxes'): for tax in doc.get('taxes'):
found = False found = False
for t in taxes: for t in taxes:
@@ -162,7 +163,7 @@ class POSInvoiceMergeLog(Document):
invoice.ignore_pricing_rule = 1 invoice.ignore_pricing_rule = 1
return invoice return invoice
def get_new_sales_invoice(self): def get_new_sales_invoice(self):
sales_invoice = frappe.new_doc('Sales Invoice') sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer sales_invoice.customer = self.customer
@@ -194,7 +195,7 @@ def get_all_unconsolidated_invoices():
} }
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer']) fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
return pos_invoices return pos_invoices
def get_invoice_customer_map(pos_invoices): def get_invoice_customer_map(pos_invoices):
@@ -204,7 +205,7 @@ def get_invoice_customer_map(pos_invoices):
customer = invoice.get('customer') customer = invoice.get('customer')
pos_invoice_customer_map.setdefault(customer, []) pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice) pos_invoice_customer_map[customer].append(invoice)
return pos_invoice_customer_map return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}): def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):

View File

@@ -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, set_gl_entries_by_account(start_date,
end_date, root.lft, root.rgt, filters, 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) calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
accumulate_values_into_parents(accounts, accounts_by_name, companies) 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 return data
def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, 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], ... }""" """Returns a dict like { "account": [gl entries], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company', 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: for entry in gl_entries:
key = entry.account_number or entry.account_name 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) gl_entries_by_account.setdefault(key, []).append(entry)
return gl_entries_by_account 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: if key not in accounts_by_name:
field = "Account number" if entry.account_number else "Account name" args = get_account_details(entry.account)
frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
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): def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = [] additional_conditions = []

View File

@@ -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)) error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
if company: if company:
error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company)) error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
if verbose==1: frappe.msgprint(error_msg) if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(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, def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=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): def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabGL Entry` frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) 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(stock_vouchers, posting_date)
gle = get_voucherwise_gl_entries(future_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), []) existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_doc(voucher_type, voucher_no) voucher_obj = frappe.get_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account) 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: else:
_delete_gl_entries(voucher_type, voucher_no) _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 = [] future_stock_vouchers = []
values = [] 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))) condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += 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 for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle from `tabStock Ledger Entry` sle
where 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( 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) stock_bal, account_bal, frappe.bold(account), posting_date)
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\ 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( frappe.msgprint(
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution), msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),

View File

@@ -127,6 +127,10 @@ class RequestforQuotation(BuyingController):
'link_doctype': 'Supplier', 'link_doctype': 'Supplier',
'link_name': rfq_supplier.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: if not contact.email_id and not contact.user:
contact.email_id = user.name contact.email_id = user.name

View File

@@ -26,7 +26,6 @@
"supplier_group", "supplier_group",
"supplier_type", "supplier_type",
"pan", "pan",
"language",
"allow_purchase_invoice_creation_without_purchase_order", "allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt", "allow_purchase_invoice_creation_without_purchase_receipt",
"disabled", "disabled",
@@ -57,6 +56,7 @@
"website", "website",
"supplier_details", "supplier_details",
"column_break_30", "column_break_30",
"language",
"is_frozen" "is_frozen"
], ],
"fields": [ "fields": [
@@ -384,7 +384,7 @@
"idx": 370, "idx": 370,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-06-17 23:18:20", "modified": "2021-01-06 19:51:40.939087",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",

View File

@@ -1309,45 +1309,28 @@ def add_taxes_from_tax_template(child_item, parent_doc):
}) })
tax_row.db_insert() 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) 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')) item = frappe.get_doc("Item", trans_item.get('item_code'))
child_item.item_code = item.item_code for field in ("item_code", "item_name", "description", "item_group"):
child_item.item_name = item.item_name child_item.update({field: item.get(field)})
child_item.description = item.description date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_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 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")) 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.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
set_child_tax_template_and_map(item, child_item, p_doc) if child_doctype == "Purchase Order Item":
add_taxes_from_tax_template(child_item, p_doc) child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) child_item.base_amount = 1 # Initiallize value will update in parent validation
if not child_item.warehouse: if child_doctype == "Sales Order Item":
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
.format(frappe.bold("default warehouse"), frappe.bold(item.item_code))) if not child_item.warehouse:
return child_item 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)))
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
set_child_tax_template_and_map(item, child_item, p_doc) set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc) add_taxes_from_tax_template(child_item, p_doc)
return child_item 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): def get_new_child_item(item_row):
new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row) return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def validate_quantity(child_item, d): def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):

View File

@@ -204,8 +204,6 @@ def get_already_returned_items(doc):
return items return items
def get_returned_qty_map_for_row(row_name, doctype): def get_returned_qty_map_for_row(row_name, doctype):
if doctype == "POS Invoice": return {}
child_doctype = doctype + " Item" child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype) 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.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account 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 target_doc.price_list_rate = 0
if default_warehouse_for_sales_return: if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return target_doc.warehouse = default_warehouse_for_sales_return

View File

@@ -488,13 +488,12 @@ class StockController(AccountsController):
"voucher_no": self.name, "voucher_no": self.name,
"company": self.company "company": self.company
}) })
if check_if_future_sle_exists(args): if check_if_future_sle_exists(args):
create_repost_item_valuation_entry(args) create_repost_item_valuation_entry(args)
elif not is_reposting_pending(): elif not is_reposting_pending():
check_if_stock_and_account_balance_synced(self.posting_date, check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name) self.company, self.doctype, self.name)
def is_reposting_pending(): def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation", return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]}) {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})

View File

@@ -15,6 +15,8 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_ra
class calculate_taxes_and_totals(object): class calculate_taxes_and_totals(object):
def __init__(self, doc): def __init__(self, doc):
self.doc = 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() self.calculate()
def calculate(self): def calculate(self):
@@ -332,10 +334,18 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity": elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty 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) self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return 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): def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item # store tax breakup for each item
key = item.item_code or item.item_name 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 @erpnext.allow_regional
def update_itemised_tax_data(doc): def update_itemised_tax_data(doc):

View File

@@ -49,6 +49,7 @@
"phone", "phone",
"mobile_no", "mobile_no",
"fax", "fax",
"website",
"more_info", "more_info",
"type", "type",
"market_segment", "market_segment",
@@ -56,8 +57,8 @@
"request_type", "request_type",
"column_break3", "column_break3",
"company", "company",
"website",
"territory", "territory",
"language",
"unsubscribed", "unsubscribed",
"blog_subscriber", "blog_subscriber",
"title" "title"
@@ -447,13 +448,19 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Address Type", "label": "Address Type",
"options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther" "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", "icon": "fa fa-user",
"idx": 5, "idx": 5,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-10-13 15:24:00.094811", "modified": "2021-01-06 19:39:58.748978",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead", "name": "Lead",

View File

@@ -54,6 +54,7 @@
"campaign", "campaign",
"column_break1", "column_break1",
"transaction_date", "transaction_date",
"language",
"amended_from", "amended_from",
"lost_reasons" "lost_reasons"
], ],
@@ -419,12 +420,18 @@
"fieldtype": "Duration", "fieldtype": "Duration",
"label": "First Response Time", "label": "First Response Time",
"read_only": 1 "read_only": 1
},
{
"fieldname": "language",
"fieldtype": "Link",
"label": "Print Language",
"options": "Language"
} }
], ],
"icon": "fa fa-info-sign", "icon": "fa fa-info-sign",
"idx": 195, "idx": 195,
"links": [], "links": [],
"modified": "2020-08-12 17:34:35.066961", "modified": "2021-01-06 19:42:46.190051",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Opportunity", "name": "Opportunity",

View File

@@ -2,4 +2,82 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Appointment Type', { 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);
}
}
});
}
}
});

View File

@@ -12,7 +12,10 @@
"appointment_type", "appointment_type",
"ip", "ip",
"default_duration", "default_duration",
"color" "color",
"billing_section",
"price_list",
"items"
], ],
"fields": [ "fields": [
{ {
@@ -52,10 +55,27 @@
"label": "Color", "label": "Color",
"no_copy": 1, "no_copy": 1,
"report_hide": 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": [], "links": [],
"modified": "2020-02-03 21:06:05.833050", "modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Appointment Type", "name": "Appointment Type",

View File

@@ -4,6 +4,53 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from frappe.model.document import Document from frappe.model.document import Document
import frappe
class AppointmentType(Document): 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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -121,6 +121,7 @@ class ClinicalProcedure(Document):
stock_entry.stock_entry_type = 'Material Receipt' stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.to_warehouse = self.warehouse stock_entry.to_warehouse = self.warehouse
stock_entry.company = self.company
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company) expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
for item in self.items: for item in self.items:
if item.qty > item.actual_qty: if item.qty > item.actual_qty:

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2017, ESS LLP and Contributors # Copyright (c) 2017, ESS LLP and Contributors
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
@@ -60,6 +60,7 @@ def create_procedure(procedure_template, patient, practitioner):
procedure.practitioner = practitioner procedure.practitioner = practitioner
procedure.consume_stock = procedure_template.allow_stock_consumption procedure.consume_stock = procedure_template.allow_stock_consumption
procedure.items = procedure_template.items 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() procedure.submit()
return procedure return procedure

View File

@@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge", "fieldname": "op_consulting_charge",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Out Patient Consulting Charge", "label": "Out Patient Consulting Charge",
"mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency" "options": "Currency"
}, },
{ {
@@ -174,7 +175,8 @@
{ {
"fieldname": "inpatient_visit_charge", "fieldname": "inpatient_visit_charge",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Inpatient Visit Charge" "label": "Inpatient Visit Charge",
"mandatory_depends_on": "inpatient_visit_charge_item"
}, },
{ {
"depends_on": "eval: !doc.__islocal", "depends_on": "eval: !doc.__islocal",
@@ -280,7 +282,7 @@
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-04-06 13:44:24.759623", "modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Practitioner", "name": "Healthcare Practitioner",

View File

@@ -24,11 +24,13 @@ frappe.ui.form.on('Patient Appointment', {
}); });
frm.set_query('practitioner', function() { frm.set_query('practitioner', function() {
return { if (frm.doc.department) {
filters: { return {
'department': frm.doc.department filters: {
} 'department': frm.doc.department
}; }
};
}
}); });
frm.set_query('service_unit', function() { frm.set_query('service_unit', function() {
@@ -140,6 +142,20 @@ frappe.ui.form.on('Patient Appointment', {
patient: function(frm) { patient: function(frm) {
if (frm.doc.patient) { if (frm.doc.patient) {
frm.trigger('toggle_payment_fields'); 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 { } else {
frm.set_value('patient_name', ''); frm.set_value('patient_name', '');
frm.set_value('patient_sex', ''); 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) { therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter'); frm.trigger('set_therapy_type_filter');
}, },
@@ -190,14 +237,18 @@ frappe.ui.form.on('Patient Appointment', {
// show payment fields as non-mandatory // show payment fields as non-mandatory
frm.toggle_display('mode_of_payment', 0); frm.toggle_display('mode_of_payment', 0);
frm.toggle_display('paid_amount', 0); frm.toggle_display('paid_amount', 0);
frm.toggle_display('billing_item', 0);
frm.toggle_reqd('mode_of_payment', 0); frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0); frm.toggle_reqd('paid_amount', 0);
frm.toggle_reqd('billing_item', 0);
} else { } else {
// if automated appointment invoicing is disabled, hide fields // if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0); frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
frm.toggle_display('paid_amount', 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('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', 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 calculate_age = function(birth) {
let ageMS = Date.parse(Date()) - Date.parse(birth); let ageMS = Date.parse(Date()) - Date.parse(birth);
let age = new Date(); let age = new Date();

View File

@@ -19,19 +19,19 @@
"inpatient_record", "inpatient_record",
"column_break_1", "column_break_1",
"company", "company",
"practitioner",
"practitioner_name",
"department",
"service_unit", "service_unit",
"section_break_12",
"appointment_type",
"duration",
"procedure_template", "procedure_template",
"get_procedure_from_encounter", "get_procedure_from_encounter",
"procedure_prescription", "procedure_prescription",
"therapy_plan", "therapy_plan",
"therapy_type", "therapy_type",
"get_prescribed_therapies", "get_prescribed_therapies",
"practitioner",
"practitioner_name",
"department",
"section_break_12",
"appointment_type",
"duration",
"column_break_17", "column_break_17",
"appointment_date", "appointment_date",
"appointment_time", "appointment_time",
@@ -79,6 +79,7 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"fetch_from": "appointment_type.default_duration",
"fieldname": "duration", "fieldname": "duration",
"fieldtype": "Int", "fieldtype": "Int",
"in_filter": 1, "in_filter": 1,
@@ -144,7 +145,6 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Healthcare Practitioner", "label": "Healthcare Practitioner",
"options": "Healthcare Practitioner", "options": "Healthcare Practitioner",
"read_only": 1,
"reqd": 1, "reqd": 1,
"search_index": 1, "search_index": 1,
"set_only_once": 1 "set_only_once": 1
@@ -158,7 +158,6 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Department", "label": "Department",
"options": "Medical Department", "options": "Medical Department",
"read_only": 1,
"search_index": 1, "search_index": 1,
"set_only_once": 1 "set_only_once": 1
}, },
@@ -227,12 +226,14 @@
"fieldname": "mode_of_payment", "fieldname": "mode_of_payment",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Mode of Payment", "label": "Mode of Payment",
"options": "Mode of Payment" "options": "Mode of Payment",
"read_only_depends_on": "invoiced"
}, },
{ {
"fieldname": "paid_amount", "fieldname": "paid_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Paid Amount" "label": "Paid Amount",
"read_only_depends_on": "invoiced"
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
@@ -302,7 +303,8 @@
"fieldname": "therapy_plan", "fieldname": "therapy_plan",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Therapy Plan", "label": "Therapy Plan",
"options": "Therapy Plan" "options": "Therapy Plan",
"set_only_once": 1
}, },
{ {
"fieldname": "ref_sales_invoice", "fieldname": "ref_sales_invoice",
@@ -347,7 +349,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-12-16 13:16:58.578503", "modified": "2021-02-08 13:13:15.116833",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Appointment", "name": "Patient Appointment",

View File

@@ -26,6 +26,7 @@ class PatientAppointment(Document):
def after_insert(self): def after_insert(self):
self.update_prescription_details() self.update_prescription_details()
self.set_payment_details()
invoice_appointment(self) invoice_appointment(self)
self.update_fee_validity() self.update_fee_validity()
send_confirmation_msg(self) send_confirmation_msg(self)
@@ -85,6 +86,13 @@ class PatientAppointment(Document):
def set_appointment_datetime(self): def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00") 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): def validate_customer_created(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'): if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
if not frappe.db.get_value('Patient', self.patient, 'customer'): if not frappe.db.get_value('Patient', self.patient, 'customer'):
@@ -148,31 +156,37 @@ def invoice_appointment(appointment_doc):
fee_validity = None fee_validity = None
if automate_invoicing and not appointment_invoiced and not fee_validity: if automate_invoicing and not appointment_invoiced and not fee_validity:
sales_invoice = frappe.new_doc('Sales Invoice') create_sales_invoice(appointment_doc)
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)
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 def create_sales_invoice(appointment_doc):
if appointment_doc.mode_of_payment and appointment_doc.paid_amount: sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.is_pos = 1 sales_invoice.patient = appointment_doc.patient
payment = sales_invoice.append('payments', {}) sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
payment.mode_of_payment = appointment_doc.mode_of_payment sales_invoice.appointment = appointment_doc.name
payment.amount = appointment_doc.paid_amount 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) item = sales_invoice.append('items', {})
sales_invoice.flags.ignore_mandatory = True item = get_appointment_item(appointment_doc, item)
sales_invoice.save(ignore_permissions=True)
sales_invoice.submit() # Add payments if payment details are supplied else proceed to create invoice as Unpaid
frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True) if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) sales_invoice.is_pos = 1
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) 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): 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): def get_appointment_item(appointment_doc, item):
service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc) details = get_service_item_and_practitioner_charge(appointment_doc)
item.item_code = service_item 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.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner)
item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company) 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.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center')
item.rate = practitioner_charge item.rate = charge
item.amount = practitioner_charge item.amount = charge
item.qty = 1 item.qty = 1
item.reference_dt = 'Patient Appointment' item.reference_dt = 'Patient Appointment'
item.reference_dn = appointment_doc.name item.reference_dn = appointment_doc.name

View File

@@ -32,7 +32,8 @@ class TestPatientAppointment(unittest.TestCase):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1) frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 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) encounter = make_encounter(appointment.name)
self.assertTrue(encounter) self.assertTrue(encounter)
self.assertEqual(encounter.company, appointment.company) self.assertEqual(encounter.company, appointment.company)
@@ -41,7 +42,7 @@ class TestPatientAppointment(unittest.TestCase):
# invoiced flag mapped from appointment # invoiced flag mapped from appointment
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced')) 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() 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, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 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, 'patient'), appointment.patient)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount) 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): def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs() patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1) frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
@@ -178,14 +223,15 @@ def create_encounter(appointment):
encounter.submit() encounter.submit()
return encounter 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() item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item) frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
appointment = frappe.new_doc('Patient Appointment') appointment = frappe.new_doc('Patient Appointment')
appointment.patient = patient appointment.patient = patient
appointment.practitioner = practitioner appointment.practitioner = practitioner
appointment.department = '_Test Medical Department' appointment.department = department or '_Test Medical Department'
appointment.appointment_date = appointment_date appointment.appointment_date = appointment_date
appointment.company = '_Test Company' appointment.company = '_Test Company'
appointment.duration = 15 appointment.duration = 15
@@ -193,7 +239,8 @@ def create_appointment(patient, practitioner, appointment_date, invoice=0, proce
appointment.service_unit = service_unit appointment.service_unit = service_unit
if invoice: if invoice:
appointment.mode_of_payment = 'Cash' appointment.mode_of_payment = 'Cash'
appointment.paid_amount = 500 if appointment_type:
appointment.appointment_type = appointment_type
if procedure_template: if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name') appointment.procedure_template = create_clinical_procedure_template().get('name')
if save: if save:
@@ -223,4 +270,29 @@ def create_clinical_procedure_template():
template.description = 'Knee Surgery and Rehab' template.description = 'Knee Surgery and Rehab'
template.rate = 50000 template.rate = 50000
template.save() template.save()
return template 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()

View File

@@ -5,9 +5,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import math import math
import frappe import frappe
import json
from frappe import _ from frappe import _
from frappe.utils.formatters import format_value from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded 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.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple 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 income_account = None
service_item = None service_item = None
if appointment.practitioner: 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) income_account = get_income_account(appointment.practitioner, appointment.company)
appointments_to_invoice.append({ appointments_to_invoice.append({
'reference_type': 'Patient Appointment', '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'): frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue 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) income_account = get_income_account(encounter.practitioner, encounter.company)
encounters_to_invoice.append({ 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 \ if procedure.invoice_separately_as_consumables and procedure.consume_stock \
and procedure.status == 'Completed' and not procedure.consumption_invoiced: 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: if not service_item:
msg = _('Please Configure Clinical Procedure Consumable Item in ') msg = _('Please Configure Clinical Procedure Consumable Item in ')
msg += '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>''' msg += '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>'''
@@ -304,24 +310,50 @@ def get_therapy_sessions_to_invoice(patient, company):
return therapy_sessions_to_invoice return therapy_sessions_to_invoice
@frappe.whitelist()
def get_service_item_and_practitioner_charge(doc): 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 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: if not service_item:
service_item = get_healthcare_service_item('inpatient_visit_charge_item') service_item = get_healthcare_service_item(is_inpatient)
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')
if not service_item: if not service_item:
throw_config_service_item(is_inpatient) throw_config_service_item(is_inpatient)
practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
if not practitioner_charge: if not practitioner_charge:
throw_config_practitioner_charge(is_inpatient, doc.practitioner) 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 return service_item, practitioner_charge
@@ -345,12 +377,27 @@ def throw_config_practitioner_charge(is_inpatient, practitioner):
frappe.throw(msg, title=_('Missing Configuration')) frappe.throw(msg, title=_('Missing Configuration'))
def get_practitioner_service_item(practitioner, service_item_field): def get_practitioner_service_item(practitioner, is_inpatient):
return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field) 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): def get_healthcare_service_item(is_inpatient):
return frappe.db.get_single_value('Healthcare Settings', service_item_field) 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): def get_practitioner_charge(practitioner, is_inpatient):
@@ -381,7 +428,8 @@ def set_invoiced(item, method, ref_invoice=None):
invoiced = True invoiced = True
if item.reference_dt == 'Clinical Procedure': 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) frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced)
else: else:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced) 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): 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') is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced')
else: else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced') is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')

View File

@@ -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_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.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.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_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.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', 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',

View File

@@ -267,6 +267,17 @@ class JobCard(Document):
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], 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}) 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): def set_transferred_qty(self, update_status=False):
if not self.items: if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 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', { self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name, 'job_card': self.name,
'work_order': self.work_order, 'work_order': self.work_order,
'docstatus': 1 'docstatus': 1,
'purpose': 'Material Transfer for Manufacture'
}, 'sum(fg_completed_qty)') or 0 }, 'sum(fg_completed_qty)') or 0
self.db_set("transferred_qty", self.transferred_qty) 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.purpose = "Material Transfer for Manufacture"
target.from_bom = 1 target.from_bom = 1
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
target.set_transfer_qty()
target.calculate_rate_and_amount() target.calculate_rate_and_amount()
target.set_missing_values() target.set_missing_values()
target.set_stock_entry_type() target.set_stock_entry_type()
@@ -437,9 +450,10 @@ def make_stock_entry(source_name, target_doc=None):
"field_map": { "field_map": {
"source_warehouse": "s_warehouse", "source_warehouse": "s_warehouse",
"required_qty": "qty", "required_qty": "qty",
"uom": "stock_uom" "name": "job_card_item"
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: doc.required_qty > 0
} }
}, target_doc, set_missing_values) }, target_doc, set_missing_values)

View File

@@ -1,363 +1,120 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2018-07-09 17:20:44.737289",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2018-07-09 17:20:44.737289", "field_order": [
"custom": 0, "item_code",
"docstatus": 0, "source_warehouse",
"doctype": "DocType", "uom",
"document_type": "", "item_group",
"editable_grid": 1, "column_break_3",
"engine": "InnoDB", "stock_uom",
"item_name",
"description",
"qty_section",
"required_qty",
"column_break_9",
"transferred_qty",
"allow_alternative_item"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "item_code",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Item Code",
"collapsible": 0, "options": "Item",
"columns": 0, "read_only": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "source_warehouse",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "ignore_user_permissions": 1,
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Source Warehouse",
"columns": 0, "options": "Warehouse"
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "uom",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "UOM",
"bold": 0, "options": "UOM"
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "item_name",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "label": "Item Name",
"bold": 0, "read_only": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "description",
"allow_in_quick_entry": 0, "fieldtype": "Text",
"allow_on_submit": 0, "label": "Description",
"bold": 0, "read_only": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "qty_section",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Qty"
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "required_qty",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Required Qty",
"collapsible": 0, "read_only": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_9",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"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
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "fieldname": "allow_alternative_item",
"allow_on_submit": 0, "fieldtype": "Check",
"bold": 0, "label": "Allow Alternative Item"
"collapsible": 0, },
"columns": 0, {
"fieldname": "allow_alternative_item", "fetch_from": "item_code.item_group",
"fieldtype": "Check", "fieldname": "item_group",
"hidden": 0, "fieldtype": "Link",
"ignore_user_permissions": 0, "label": "Item Group",
"ignore_xss_filter": 0, "options": "Item Group",
"in_filter": 0, "read_only": 1
"in_global_search": 0, },
"in_list_view": 0, {
"in_standard_filter": 0, "fetch_from": "item_code.stock_uom",
"label": "Allow Alternative Item", "fieldname": "stock_uom",
"length": 0, "fieldtype": "Link",
"no_copy": 0, "label": "Stock UOM",
"permlevel": 0, "options": "UOM"
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fieldname": "transferred_qty",
"read_only": 0, "fieldtype": "Float",
"remember_last_selected_value": 0, "label": "Transferred Qty",
"report_hide": 0, "no_copy": 1,
"reqd": 0, "print_hide": 1,
"search_index": 0, "read_only": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "istable": 1,
"hide_toolbar": 0, "links": [],
"idx": 0, "modified": "2021-02-11 13:50:13.804108",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Manufacturing",
"is_submittable": 0, "name": "Job Card Item",
"issingle": 0, "owner": "Administrator",
"istable": 1, "permissions": [],
"max_attachments": 0, "quick_entry": 1,
"modified": "2018-08-28 15:23:48.099459", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Manufacturing", "track_changes": 1
"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
} }

View File

@@ -94,11 +94,11 @@ class TestWorkOrder(unittest.TestCase):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2, wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=warehouse, skip_transfer=1) 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 # reserved qty for production is updated
self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
cint(bin1_on_submit.reserved_qty_for_production))
test_stock_entry.make_stock_entry(item_code="_Test Item", test_stock_entry.make_stock_entry(item_code="_Test Item",
target=warehouse, qty=100, basic_rate=100) target=warehouse, qty=100, basic_rate=100)
@@ -109,9 +109,9 @@ class TestWorkOrder(unittest.TestCase):
s.submit() s.submit()
bin1_at_completion = get_bin(item, warehouse) bin1_at_completion = get_bin(item, warehouse)
self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production), 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): def test_production_item(self):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True) wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)

View File

@@ -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.convert_qi_parameter_to_link_field
erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes 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.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

View File

@@ -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()

View File

@@ -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

View File

@@ -1103,10 +1103,10 @@ class SalarySlip(TransactionBase):
self.calculate_total_for_salary_slip_based_on_timesheet() self.calculate_total_for_salary_slip_based_on_timesheet()
else: else:
self.total_deduction = 0.0 self.total_deduction = 0.0
if self.earnings: if hasattr(self, "earnings"):
for earning in self.earnings: for earning in self.earnings:
self.gross_pay += flt(earning.amount, earning.precision("amount")) self.gross_pay += flt(earning.amount, earning.precision("amount"))
if self.deductions: if hasattr(self, "deductions"):
for deduction in self.deductions: for deduction in self.deductions:
self.total_deduction += flt(deduction.amount, deduction.precision("amount")) 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) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -2,7 +2,9 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
erpnext.taxes_and_totals = erpnext.payments.extend({ erpnext.taxes_and_totals = erpnext.payments.extend({
setup: function() {}, setup: function() {
this.fetch_round_off_accounts();
},
apply_pricing_rule_on_item: function(item) { apply_pricing_rule_on_item: function(item) {
let effective_item_rate = item.price_list_rate; 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() { determine_exclusive_rate: function() {
var me = this; var me = this;
@@ -372,11 +390,21 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
} else if (tax.charge_type == "On Item Quantity") { } else if (tax.charge_type == "On Item Quantity") {
current_tax_amount = tax_rate * item.qty; 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); this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
return 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) { set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) {
// store tax breakup for each item // store tax breakup for each item
let tax_detail = tax.item_wise_tax_detail; let tax_detail = tax.item_wise_tax_detail;

View File

@@ -140,6 +140,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
() => me.update_batch_serial_no_items(), () => me.update_batch_serial_no_items(),
() => { () => {
refresh_field("items"); refresh_field("items");
refresh_field("packed_items");
if (me.callback) { if (me.callback) {
return me.callback(me.item); return me.callback(me.item);
} }
@@ -154,7 +155,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
if (this.item.serial_no) { if (this.item.serial_no) {
this.dialog.fields_dict.serial_no.set_value(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) { if (this.has_batch && !this.has_serial_no && d.batch_no) {
this.frm.doc.items.forEach(data => { this.frm.doc.items.forEach(data => {
if(data.item_code == d.item_code) { if(data.item_code == d.item_code) {
@@ -231,7 +232,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
this.map_row_values(row, batch, 'batch_no', this.map_row_values(row, batch, 'batch_no',
'selected_qty', this.values.warehouse); 'selected_qty', this.values.warehouse);
}); });
} }
}, },
update_serial_no_item() { update_serial_no_item() {
@@ -250,7 +251,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
filters: { 'name': ["in", selected_serial_nos]}, filters: { 'name': ["in", selected_serial_nos]},
fields: ["batch_no", "name"] fields: ["batch_no", "name"]
}).then((data) => { }).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"}] // {batch_no: 'batch-2', name: "SR-003"}, {batch_no: 'batch-2', name: "SR-004"}]
const batch_serial_map = data.reduce((acc, d) => { const batch_serial_map = data.reduce((acc, d) => {
if (!acc[d['batch_no']]) acc[d['batch_no']] = []; if (!acc[d['batch_no']]) acc[d['batch_no']] = [];
@@ -298,6 +299,8 @@ erpnext.SerialNoBatchSelector = Class.extend({
} else { } else {
row.warehouse = values.warehouse || warehouse; row.warehouse = values.warehouse || warehouse;
} }
this.frm.dirty();
}, },
update_total_qty: function() { update_total_qty: function() {

View File

@@ -32,7 +32,12 @@ body[data-route*="marketplace"] {
} }
.hub-image-loading, .hub-image-broken { .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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -1,222 +1,86 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2017-06-27 15:09:01.318003",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2017-06-27 15:09:01.318003", "field_order": [
"custom": 0, "gst_summary",
"docstatus": 0, "column_break_2",
"doctype": "DocType", "round_off_gst_values",
"document_type": "", "gstin_email_sent_on",
"editable_grid": 1, "section_break_4",
"engine": "InnoDB", "gst_accounts",
"b2c_limit"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "gst_summary",
"allow_on_submit": 0, "fieldtype": "HTML",
"bold": 0, "label": "GST Summary",
"collapsible": 0, "show_days": 1,
"columns": 0, "show_seconds": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_2",
"allow_on_submit": 0, "fieldtype": "Column Break",
"bold": 0, "show_days": 1,
"collapsible": 0, "show_seconds": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "gstin_email_sent_on",
"allow_on_submit": 0, "fieldtype": "Date",
"bold": 0, "label": "GSTIN Email Sent On",
"collapsible": 0, "read_only": 1,
"columns": 0, "show_days": 1,
"fieldname": "gstin_email_sent_on", "show_seconds": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_4",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "show_days": 1,
"collapsible": 0, "show_seconds": 1
"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
},
{ {
"allow_bulk_edit": 0, "fieldname": "gst_accounts",
"allow_on_submit": 0, "fieldtype": "Table",
"bold": 0, "label": "GST Accounts",
"collapsible": 0, "options": "GST Account",
"columns": 0, "show_days": 1,
"fieldname": "gst_accounts", "show_seconds": 1
"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
},
{ {
"allow_bulk_edit": 0, "default": "250000",
"allow_on_submit": 0, "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
"bold": 0, "fieldname": "b2c_limit",
"collapsible": 0, "fieldtype": "Data",
"columns": 0, "in_list_view": 1,
"default": "250000", "label": "B2C Limit",
"description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.", "reqd": 1,
"fieldname": "b2c_limit", "show_days": 1,
"fieldtype": "Data", "show_seconds": 1
"hidden": 0, },
"ignore_user_permissions": 0, {
"ignore_xss_filter": 0, "default": "0",
"in_filter": 0, "description": "Enabling this option will round off individual GST components in all the Invoices",
"in_global_search": 0, "fieldname": "round_off_gst_values",
"in_list_view": 1, "fieldtype": "Check",
"in_standard_filter": 0, "label": "Round Off GST Values",
"label": "B2C Limit", "show_days": 1,
"length": 0, "show_seconds": 1
"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
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "issingle": 1,
"hide_toolbar": 0, "links": [],
"idx": 0, "modified": "2021-01-28 17:19:47.969260",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Regional",
"is_submittable": 0, "name": "GST Settings",
"issingle": 1, "owner": "Administrator",
"istable": 0, "permissions": [],
"max_attachments": 0, "quick_entry": 1,
"modified": "2018-02-14 08:14:15.375181", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Regional", "track_changes": 1
"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
}

View File

@@ -14,8 +14,20 @@ import json
test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"] test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"]
class TestGSTR3BReport(unittest.TestCase): 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 = { month_number_mapping = {
1: "January", 1: "January",
2: "February", 2: "February",
@@ -31,17 +43,6 @@ class TestGSTR3BReport(unittest.TestCase):
12: "December" 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() make_sales_invoice()
create_purchase_invoices() 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]["samt"], 22.50)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 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(): def make_sales_invoice():
si = create_sales_invoice(company="_Test Company GST", si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer', customer = '_Test GST Customer',
@@ -145,7 +182,6 @@ def make_sales_invoice():
si3.submit() si3.submit()
def create_purchase_invoices(): def create_purchase_invoices():
pi = make_purchase_invoice( pi = make_purchase_invoice(
company="_Test Company GST", company="_Test Company GST",
supplier = '_Test Registered Supplier', supplier = '_Test Registered Supplier',
@@ -193,7 +229,6 @@ def create_purchase_invoices():
pi1.submit() pi1.submit()
def make_suppliers(): def make_suppliers():
if not frappe.db.exists("Supplier", "_Test Registered Supplier"): if not frappe.db.exists("Supplier", "_Test Registered Supplier"):
frappe.get_doc({ frappe.get_doc({
"supplier_group": "_Test Supplier Group", "supplier_group": "_Test Supplier Group",
@@ -257,7 +292,6 @@ def make_suppliers():
address.save() address.save()
def make_customers(): def make_customers():
if not frappe.db.exists("Customer", "_Test GST Customer"): if not frappe.db.exists("Customer", "_Test GST Customer"):
frappe.get_doc({ frappe.get_doc({
"customer_group": "_Test Customer Group", "customer_group": "_Test Customer Group",
@@ -354,9 +388,9 @@ def make_customers():
address.save() address.save()
def make_company(): def make_company():
if frappe.db.exists("Company", "_Test Company GST"): if frappe.db.exists("Company", "_Test Company GST"):
return return
company = frappe.new_doc("Company") company = frappe.new_doc("Company")
company.company_name = "_Test Company GST" company.company_name = "_Test Company GST"
company.abbr = "_GST" company.abbr = "_GST"
@@ -388,7 +422,6 @@ def make_company():
address.save() address.save()
def set_account_heads(): def set_account_heads():
gst_settings = frappe.get_doc("GST Settings") gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all( gst_account = frappe.get_all(

View File

@@ -5,12 +5,16 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import getdate from frappe.utils import getdate, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
class LowerDeductionCertificate(Document): class LowerDeductionCertificate(Document):
def validate(self): 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): if getdate(self.valid_upto) < getdate(self.valid_from):
frappe.throw(_("Valid Upto date cannot be before Valid From date")) frappe.throw(_("Valid Upto date cannot be before Valid From date"))
@@ -24,3 +28,20 @@ class LowerDeductionCertificate(Document):
<= fiscal_year.year_end_date): <= fiscal_year.year_end_date):
frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) 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

View File

@@ -20,6 +20,7 @@ states = [
'Jharkhand', 'Jharkhand',
'Karnataka', 'Karnataka',
'Kerala', 'Kerala',
'Ladakh',
'Lakshadweep Islands', 'Lakshadweep Islands',
'Madhya Pradesh', 'Madhya Pradesh',
'Maharashtra', 'Maharashtra',
@@ -59,6 +60,7 @@ state_numbers = {
"Jharkhand": "20", "Jharkhand": "20",
"Karnataka": "29", "Karnataka": "29",
"Kerala": "32", "Kerala": "32",
"Ladakh": "38",
"Lakshadweep Islands": "31", "Lakshadweep Islands": "31",
"Madhya Pradesh": "23", "Madhya Pradesh": "23",
"Maharashtra": "27", "Maharashtra": "27",
@@ -80,4 +82,4 @@ state_numbers = {
"West Bengal": "19", "West Bengal": "19",
} }
number_state_mapping = {v: k for k, v in iteritems(state_numbers)} number_state_mapping = {v: k for k, v in iteritems(state_numbers)}

View File

@@ -168,5 +168,10 @@
"state_number": "37", "state_number": "37",
"state_code": "AD", "state_code": "AD",
"state_name": "Andhra Pradesh (New)" "state_name": "Andhra Pradesh (New)"
},
{
"state_number": "38",
"state_code": "LA",
"state_name": "Ladakh"
} }
] ]

View File

@@ -772,3 +772,24 @@ def make_regional_gl_entries(gl_entries, doc):
) )
return gl_entries 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

View File

@@ -236,6 +236,7 @@ class Gstr1Report(object):
self.cgst_sgst_invoices = [] self.cgst_sgst_invoices = []
unidentified_gst_accounts = [] unidentified_gst_accounts = []
unidentified_gst_accounts_invoice = []
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
if account in self.gst_accounts.cess_account: if account in self.gst_accounts.cess_account:
self.invoice_cess.setdefault(parent, tax_amount) 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 not (cgst_or_sgst or account in self.gst_accounts.igst_account):
if "gst" in account.lower() and account not in unidentified_gst_accounts: if "gst" in account.lower() and account not in unidentified_gst_accounts:
unidentified_gst_accounts.append(account) unidentified_gst_accounts.append(account)
unidentified_gst_accounts_invoice.append(parent)
continue continue
for item_code, tax_amounts in item_wise_tax_detail.items(): 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 # Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items): 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": 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()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())

View File

@@ -34,9 +34,8 @@
"companies", "companies",
"currency_and_price_list", "currency_and_price_list",
"default_currency", "default_currency",
"default_price_list",
"column_break_14", "column_break_14",
"language", "default_price_list",
"address_contacts", "address_contacts",
"address_html", "address_html",
"website", "website",
@@ -59,6 +58,7 @@
"column_break_45", "column_break_45",
"market_segment", "market_segment",
"industry", "industry",
"language",
"is_frozen", "is_frozen",
"column_break_38", "column_break_38",
"loyalty_program", "loyalty_program",
@@ -485,7 +485,7 @@
"idx": 363, "idx": 363,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-03-17 11:03:42.706907", "modified": "2021-01-06 19:35:25.418017",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@@ -514,7 +514,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
make_delivery_note: function() { make_delivery_note: function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note", method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
frm: me.frm frm: this.frm
}) })
}, },

View File

@@ -180,6 +180,7 @@ class SalesOrder(SellingController):
update_coupon_code_count(self.coupon_code,'used') update_coupon_code_count(self.coupon_code,'used')
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
super(SalesOrder, self).on_cancel() super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO # Cannot cancel closed SO

View File

@@ -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 from erpnext.stock.doctype.item.test_item import make_item
class TestSalesOrder(unittest.TestCase): 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): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
@@ -1049,6 +1061,38 @@ class TestSalesOrder(unittest.TestCase):
self.assertRaises(frappe.LinkExistsError, so_doc.cancel) 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): def test_request_for_raw_materials(self):
item = make_item("_Test Finished Item", {"is_stock_item": 1, item = make_item("_Test Finished Item", {"is_stock_item": 1,
"maintain_stock": 1, "maintain_stock": 1,
@@ -1207,4 +1251,4 @@ def make_sales_order_workflow():
)) ))
workflow.insert(ignore_permissions=True) workflow.insert(ignore_permissions=True)
return workflow return workflow

View File

@@ -28,7 +28,7 @@ def delete_company_transactions(company_name):
"Party Account", "Employee", "Sales Taxes and Charges Template", "Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM", "Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "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) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@@ -462,6 +462,9 @@ def get_party(user=None):
return customer return customer
def get_debtors_account(cart_settings): 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 = \ payment_gateway_account_currency = \
frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency

View File

@@ -26,10 +26,10 @@
"quotation_series", "quotation_series",
"section_break_8", "section_break_8",
"enable_checkout", "enable_checkout",
"payment_success_url",
"column_break_11",
"save_quotations_as_draft", "save_quotations_as_draft",
"payment_gateway_account" "column_break_11",
"payment_gateway_account",
"payment_success_url"
], ],
"fields": [ "fields": [
{ {
@@ -143,10 +143,12 @@
}, },
{ {
"default": "Orders", "default": "Orders",
"depends_on": "enable_checkout",
"description": "After payment completion redirect user to selected page.", "description": "After payment completion redirect user to selected page.",
"fieldname": "payment_success_url", "fieldname": "payment_success_url",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Payment Success Url", "label": "Payment Success Url",
"mandatory_depends_on": "enable_checkout",
"options": "\nOrders\nInvoices\nMy Account" "options": "\nOrders\nInvoices\nMy Account"
}, },
{ {
@@ -154,9 +156,11 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "enable_checkout",
"fieldname": "payment_gateway_account", "fieldname": "payment_gateway_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payment Gateway Account", "label": "Payment Gateway Account",
"mandatory_depends_on": "enable_checkout",
"options": "Payment Gateway Account" "options": "Payment Gateway Account"
}, },
{ {
@@ -186,7 +190,7 @@
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-02-01 18:18:54.606535", "modified": "2021-02-11 18:48:30.433058",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Shopping Cart", "module": "Shopping Cart",
"name": "Shopping Cart Settings", "name": "Shopping Cart Settings",

View File

@@ -64,10 +64,10 @@ def get_warehouse_account(warehouse, warehouse_account=None):
if not account and warehouse.company: if not account and warehouse.company:
account = get_company_default_inventory_account(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}") frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}")
.format(warehouse.name, warehouse.company)) .format(warehouse.name, warehouse.company))
return account return account
def get_company_default_inventory_account(company): 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')

View File

@@ -298,9 +298,9 @@ class TestBatch(unittest.TestCase):
self.assertEqual(details.get('price_list_rate'), 400) self.assertEqual(details.get('price_list_rate'), 400)
def create_batch(item_code, rate, create_item_price_for_batch): def create_batch(item_code, rate, create_item_price_for_batch):
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", pi = make_purchase_invoice(company="_Test Company",
warehouse= "Stores - TCP1", cost_center = "Main - TCP1", update_stock=1, warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
expense_account ="_Test Account Cost for Goods Sold - TCP1", item_code=item_code) 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}) batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name})

View File

@@ -16,8 +16,9 @@ class Bin(Document):
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin''' '''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args) self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": 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"): if not args.get("posting_date"):
args["posting_date"] = nowdate() args["posting_date"] = nowdate()
@@ -34,11 +35,13 @@ class Bin(Document):
"posting_time": args.get("posting_time"), "posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"), "voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"), "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) }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# Validate negative qty in future transactions # update qty in future ale and Validate negative qty
validate_negative_qty_in_future_sle(args) update_qty_in_future_sle(args, allow_negative_stock)
def update_qty(self, args): def update_qty(self, args):
# update the stock values (for current quantities) # 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.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_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.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
self.set_projected_qty() self.set_projected_qty()
self.db_update() self.db_update()

View File

@@ -489,7 +489,10 @@ class TestDeliveryNote(unittest.TestCase):
def test_closed_delivery_note(self): def test_closed_delivery_note(self):
from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status 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() dn.submit()

View File

@@ -148,7 +148,6 @@ class TestLandedCostVoucher(unittest.TestCase):
def test_landed_cost_voucher_for_odd_numbers (self): 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 = 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" pr.items[0].cost_center = "Main - TCP1"
for x in range(2): for x in range(2):
@@ -208,6 +207,10 @@ class TestLandedCostVoucher(unittest.TestCase):
self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100) self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100)
def test_multi_currency_lcv(self): 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 ## Create USD Shipping charges_account
usd_shipping = create_account(account_name="Shipping Charges USD", usd_shipping = create_account(account_name="Shipping Charges USD",
parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory", parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory",

View File

@@ -94,10 +94,15 @@ class TestPurchaseReceipt(unittest.TestCase):
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete() frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
def test_purchase_receipt_no_gl_entry(self): 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') company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item", existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"}, "stock_value") "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() pr = make_purchase_receipt()

View File

@@ -46,6 +46,9 @@ class RepostItemValuation(Document):
def repost(doc): def repost(doc):
try: try:
if not frappe.db.exists("Repost Item Valuation", doc.name):
return
doc.set_status('In Progress') doc.set_status('In Progress')
frappe.db.commit() frappe.db.commit()
@@ -64,7 +67,7 @@ def repost(doc):
message += "<br>" + "Traceback: <br>" + traceback message += "<br>" + "Traceback: <br>" + traceback
frappe.db.set_value(doc.doctype, doc.name, 'error_log', message) frappe.db.set_value(doc.doctype, doc.name, 'error_log', message)
notify_error_to_stock_managers(doc) notify_error_to_stock_managers(doc, message)
doc.set_status('Failed') doc.set_status('Failed')
raise raise
finally: finally:

View File

@@ -190,6 +190,7 @@ def create_shipment_company(company_name, abbr):
company.abbr = abbr company.abbr = abbr
company.default_currency = 'EUR' company.default_currency = 'EUR'
company.country = 'Germany' company.country = 'Germany'
company.enable_perpetual_inventory = 0
company.insert() company.insert()
return company return company

View File

@@ -163,7 +163,7 @@ class StockEntry(StockController):
if self.purpose not in valid_purposes: if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
if self.job_card and self.purpose != 'Material Transfer for Manufacture': if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']:
frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
.format(self.job_card)) .format(self.job_card))
@@ -823,6 +823,7 @@ class StockEntry(StockController):
if self.job_card: if self.job_card:
job_doc = frappe.get_doc('Job Card', self.job_card) job_doc = frappe.get_doc('Job Card', self.job_card)
job_doc.set_transferred_qty(update_status=True) job_doc.set_transferred_qty(update_status=True)
job_doc.set_transferred_qty_in_job_card(self)
if self.work_order: if self.work_order:
pro_doc = frappe.get_doc("Work Order", self.work_order) pro_doc = frappe.get_doc("Work Order", self.work_order)

View File

@@ -69,7 +69,8 @@
"putaway_rule", "putaway_rule",
"column_break_51", "column_break_51",
"reference_purchase_receipt", "reference_purchase_receipt",
"quality_inspection" "quality_inspection",
"job_card_item"
], ],
"fields": [ "fields": [
{ {
@@ -532,13 +533,22 @@
"fieldname": "is_finished_item", "fieldname": "is_finished_item",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Finished Item" "label": "Is Finished Item"
},
{
"fieldname": "job_card_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Job Card Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-30 15:00:44.489442", "modified": "2021-02-11 13:47:50.158754",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",

View File

@@ -38,6 +38,7 @@ class StockLedgerEntry(Document):
self.block_transactions_against_group_warehouse() self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time() self.validate_with_last_transaction_posting_time()
def on_submit(self): def on_submit(self):
self.check_stock_frozen_date() self.check_stock_frozen_date()
self.actual_amt_check() self.actual_amt_check()

View File

@@ -23,6 +23,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
cancel = sl_entries[0].get("is_cancelled") cancel = sl_entries[0].get("is_cancelled")
if cancel: if cancel:
validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no')) set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries: for sle in sl_entries:
@@ -45,6 +46,20 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
args = sle_doc.as_dict() args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher) update_bin(args, allow_negative_stock, via_landed_cost_voucher)
def validate_cancellation(args):
if args[0].get("is_cancelled"):
repost_entry = frappe.db.get_value("Repost Item Valuation", {
'voucher_type': args[0].voucher_type,
'voucher_no': args[0].voucher_no,
'docstatus': 1
}, ['name', 'status'], as_dict=1)
if repost_entry:
if repost_entry.status == 'In Progress':
frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
if repost_entry.status == 'Queued':
frappe.delete_doc("Repost Item Valuation", repost_entry.name)
def set_as_cancel(voucher_type, voucher_no): def set_as_cancel(voucher_type, voucher_no):
frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1, frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
@@ -74,7 +89,8 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
"item_code": args[i].item_code, "item_code": args[i].item_code,
"warehouse": args[i].warehouse, "warehouse": args[i].warehouse,
"posting_date": args[i].posting_date, "posting_date": args[i].posting_date,
"posting_time": args[i].posting_time "posting_time": args[i].posting_time,
"creation": args[i].get("creation")
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
for item_wh, new_sle in iteritems(obj.new_items): for item_wh, new_sle in iteritems(obj.new_items):
@@ -86,7 +102,7 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
def get_args_for_voucher(voucher_type, voucher_no): def get_args_for_voucher(voucher_type, voucher_no):
return frappe.db.get_all("Stock Ledger Entry", return frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time"], fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc", order_by="creation asc",
group_by="item_code, warehouse" group_by="item_code, warehouse"
) )
@@ -117,7 +133,7 @@ class update_entries_after(object):
self.item_code = args.get("item_code") self.item_code = args.get("item_code")
if self.args.sle_id: if self.args.sle_id:
self.args['name'] = self.args.sle_id self.args['name'] = self.args.sle_id
self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company") self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
self.get_precision() self.get_precision()
self.valuation_method = get_valuation_method(self.item_code) self.valuation_method = get_valuation_method(self.item_code)
@@ -155,7 +171,7 @@ class update_entries_after(object):
""" """
self.data.setdefault(args.warehouse, frappe._dict()) self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse] warehouse_dict = self.data[args.warehouse]
previous_sle = self.get_sle_before_datetime(args) previous_sle = self.get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"): for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -167,9 +183,35 @@ class update_entries_after(object):
"stock_value_difference": 0.0 "stock_value_difference": 0.0
}) })
def get_previous_sle_of_current_voucher(self, args):
"""get stock ledger entries filtered by specific posting datetime conditions"""
args['time_format'] = '%H:%i:%s'
if not args.get("posting_date"):
args["posting_date"] = "1900-01-01"
if not args.get("posting_time"):
args["posting_time"] = "00:00"
sle = frappe.db.sql("""
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %(item_code)s
and warehouse = %(warehouse)s
and is_cancelled = 0
and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by timestamp(posting_date, posting_time) desc, creation desc
limit 1""", args, as_dict=1)
return sle[0] if sle else frappe._dict()
def build(self): def build(self):
from erpnext.controllers.stock_controller import check_if_future_sle_exists
if self.args.get("sle_id"): if self.args.get("sle_id"):
self.process_sle_against_current_voucher() self.process_sle_against_current_timestamp()
if not check_if_future_sle_exists(self.args):
self.update_bin()
else: else:
entries_to_fix = self.get_future_entries_to_fix() entries_to_fix = self.get_future_entries_to_fix()
@@ -182,18 +224,20 @@ class update_entries_after(object):
if sle.dependant_sle_voucher_detail_no: if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle) entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
self.update_bin()
if self.exceptions: if self.exceptions:
self.raise_exceptions() self.raise_exceptions()
self.update_bin() def process_sle_against_current_timestamp(self):
def process_sle_against_current_voucher(self):
sl_entries = self.get_sle_against_current_voucher() sl_entries = self.get_sle_against_current_voucher()
for sle in sl_entries: for sle in sl_entries:
self.process_sle(sle) self.process_sle(sle)
def get_sle_against_current_voucher(self): def get_sle_against_current_voucher(self):
self.args['time_format'] = '%H:%i:%s'
return frappe.db.sql(""" return frappe.db.sql("""
select select
*, timestamp(posting_date, posting_time) as "timestamp" *, timestamp(posting_date, posting_time) as "timestamp"
@@ -202,7 +246,8 @@ class update_entries_after(object):
where where
item_code = %(item_code)s item_code = %(item_code)s
and warehouse = %(warehouse)s and warehouse = %(warehouse)s
and timestamp(posting_date, time_format(posting_time, '%H:%i:%s')) = timestamp(%(posting_date)s, time_format(%(posting_time)s, '%H:%i:%s')) and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
order by order by
creation ASC creation ASC
for update for update
@@ -229,7 +274,6 @@ class update_entries_after(object):
return entries_to_fix return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data: elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix return entries_to_fix
self.initialize_previous_data(dependant_sle) self.initialize_previous_data(dependant_sle)
args = self.data[dependant_sle.warehouse].previous_sle \ args = self.data[dependant_sle.warehouse].previous_sle \
@@ -636,7 +680,6 @@ class update_entries_after(object):
# update bin for each warehouse # update bin for each warehouse
for warehouse, data in iteritems(self.data): for warehouse, data in iteritems(self.data):
bin_doc = get_bin(self.item_code, warehouse) bin_doc = get_bin(self.item_code, warehouse)
bin_doc.update({ bin_doc.update({
"valuation_rate": data.valuation_rate, "valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction, "actual_qty": data.qty_after_transaction,
@@ -762,6 +805,25 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
return valuation_rate return valuation_rate
def update_qty_in_future_sle(args, allow_negative_stock=None):
frappe.db.sql("""
update `tabStock Ledger Entry`
set qty_after_transaction = qty_after_transaction + {qty}
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and voucher_no != %(voucher_no)s
and is_cancelled = 0
and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
or (
timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
and creation > %(creation)s
)
)
""".format(qty=args.actual_qty), args)
validate_negative_qty_in_future_sle(args, allow_negative_stock)
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None): def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \ allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
@@ -790,7 +852,7 @@ def get_future_sle_with_negative_qty(args):
and voucher_no != %(voucher_no)s and voucher_no != %(voucher_no)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s) and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and is_cancelled = 0 and is_cancelled = 0
and qty_after_transaction + {0} < 0 and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc order by timestamp(posting_date, posting_time) asc
limit 1 limit 1
""".format(args.actual_qty), args, as_dict=1) """, args, as_dict=1)