Merge branch 'develop' into project-template-and-tasks

This commit is contained in:
Jannat Patel 2021-01-07 13:01:02 +05:30 committed by GitHub
commit a586b0ba17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 4171 additions and 651 deletions

View File

@ -159,10 +159,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center") budget = make_budget(budget_against="Cost Center")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True) "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@ -181,10 +181,10 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project") budget = make_budget(budget_against="Project")
month = now_datetime().month month = now_datetime().month
if month > 10: if month > 9:
month = 10 month = 9
for i in range(month): for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC", jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project") "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")

View File

@ -6,14 +6,18 @@ import frappe, erpnext, json
from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form
from frappe import msgprint, _, scrub from frappe import msgprint, _, scrub
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.utils import get_balance_on, get_account_currency from erpnext.accounts.utils import get_balance_on, get_stock_accounts, get_stock_and_account_balance, \
get_account_currency, check_if_stock_and_account_balance_synced
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \
import get_party_account_based_on_invoice_discounting
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
from six import string_types, iteritems from six import string_types, iteritems
class StockAccountInvalidTransaction(frappe.ValidationError): pass
class JournalEntry(AccountsController): class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs) super(JournalEntry, self).__init__(*args, **kwargs)
@ -46,6 +50,7 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table() self.validate_empty_accounts_table()
self.set_account_and_party_balance() self.set_account_and_party_balance()
self.validate_inter_company_accounts() self.validate_inter_company_accounts()
self.validate_stock_accounts()
if not self.title: if not self.title:
self.title = self.get_title() self.title = self.get_title()
@ -57,6 +62,8 @@ class JournalEntry(AccountsController):
self.update_expense_claim() self.update_expense_claim()
self.update_inter_company_jv() self.update_inter_company_jv()
self.update_invoice_discounting() self.update_invoice_discounting()
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
def on_cancel(self): def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
@ -96,6 +103,16 @@ class JournalEntry(AccountsController):
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit: if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry")) frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
self.posting_date, self.company)
if account_bal == stock_bal:
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(account), StockAccountInvalidTransaction)
def update_inter_company_jv(self): def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference: if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\ frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\

View File

@ -6,7 +6,7 @@ import unittest, frappe
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.exceptions import InvalidAccountCurrency from erpnext.exceptions import InvalidAccountCurrency
from erpnext.accounts.general_ledger import StockAccountInvalidTransaction from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
class TestJournalEntry(unittest.TestCase): class TestJournalEntry(unittest.TestCase):
def test_journal_entry_with_against_jv(self): def test_journal_entry_with_against_jv(self):
@ -84,25 +84,31 @@ class TestJournalEntry(unittest.TestCase):
company = "_Test Company with perpetual inventory" company = "_Test Company with perpetual inventory"
stock_account = get_inventory_account(company) stock_account = get_inventory_account(company)
from erpnext.accounts.utils import get_stock_and_account_balance
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
diff = flt(account_bal) - flt(stock_bal)
if not diff:
diff = 100
jv = frappe.new_doc("Journal Entry") jv = frappe.new_doc("Journal Entry")
jv.company = company jv.company = company
jv.posting_date = nowdate() jv.posting_date = nowdate()
jv.append("accounts", { jv.append("accounts", {
"account": stock_account, "account": stock_account,
"cost_center": "Main - TCP1", "cost_center": "Main - TCP1",
"debit_in_account_currency": 100 "debit_in_account_currency": 0 if diff > 0 else abs(diff),
"credit_in_account_currency": diff if diff > 0 else 0
}) })
jv.append("accounts", { jv.append("accounts", {
"account": "Stock Adjustment - TCP1", "account": "Stock Adjustment - TCP1",
"credit_in_account_currency": 100,
"cost_center": "Main - TCP1", "cost_center": "Main - TCP1",
"debit_in_account_currency": diff if diff > 0 else 0,
"credit_in_account_currency": 0 if diff > 0 else abs(diff)
}) })
jv.insert() jv.insert()
from erpnext.accounts.utils import get_stock_and_account_balance
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
if account_bal == stock_bal: if account_bal == stock_bal:
self.assertRaises(StockAccountInvalidTransaction, jv.submit) self.assertRaises(StockAccountInvalidTransaction, jv.submit)
frappe.db.rollback() frappe.db.rollback()

View File

@ -267,6 +267,8 @@ class POSInvoice(SalesInvoice):
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile: if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {} pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name') self.pos_profile = pos_profile.get('name')
profile = {} profile = {}

View File

@ -345,9 +345,13 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency) if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
or (pricing_rule.margin_type == 'Percentage')): or (pricing_rule.margin_type == 'Percentage')):
item_details.margin_type = pricing_rule.margin_type item_details.margin_type = pricing_rule.margin_type
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
item_details.has_margin = True item_details.has_margin = True
if pricing_rule.apply_multiple_pricing_rules and item_details.margin_rate_or_amount is not None:
item_details.margin_rate_or_amount += pricing_rule.margin_rate_or_amount
else:
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
if pricing_rule.rate_or_discount == 'Rate': if pricing_rule.rate_or_discount == 'Rate':
pricing_rule_rate = 0.0 pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency: if pricing_rule.currency == args.currency:

View File

@ -164,7 +164,15 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
frappe.throw(_("Invalid {0}").format(args.get(field))) frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s` parent_groups = frappe.db.sql_list("""select name from `tab%s`
where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt)) where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
root_name = frappe.db.get_list(parenttype,
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
if root_name and root_name[0][0]:
parent_groups.append(root_name[0][0])
if parent_groups: if parent_groups:
if allow_blank: parent_groups.append('') if allow_blank: parent_groups.append('')

View File

@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %} {% include "erpnext/regional/india/taxes.js" %}
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice'); erpnext.setup_auto_gst_taxation('Sales Invoice');
erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", { frappe.ui.form.on("Sales Invoice", {
setup: function(frm) { setup: function(frm) {

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import frappe.defaults import frappe.defaults
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw from frappe import _, msgprint, throw
from erpnext.accounts.party import get_party_account, get_due_date from erpnext.accounts.party import get_party_account, get_due_date
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
@ -232,9 +232,9 @@ class SalesInvoice(SellingController):
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
def before_cancel(self): def before_cancel(self):
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None) self.update_time_sheet(None)
def on_cancel(self): def on_cancel(self):
super(SalesInvoice, self).on_cancel() super(SalesInvoice, self).on_cancel()
@ -549,7 +549,12 @@ class SalesInvoice(SellingController):
self.against_income_account = ','.join(against_acc) self.against_income_account = ','.join(against_acc)
def add_remarks(self): def add_remarks(self):
if not self.remarks: self.remarks = 'No Remarks' if not self.remarks:
if self.po_no and self.po_date:
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
formatdate(self.po_date))
else:
self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self): def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended # Don't auto set the posting date and time if invoice is amended

View File

@ -1825,93 +1825,7 @@ class TestSalesInvoice(unittest.TestCase):
# check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1)) # check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
def test_eway_bill_json(self): def test_eway_bill_json(self):
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): si = make_sales_invoice_for_ewaybill()
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address for Eway bill",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+91 0000000000",
"gstin": "27AAECE4835E1ZR",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "401108"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Customer-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+91 0000000000",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "410038"
}).insert()
address.append("links", {
"link_doctype": "Customer",
"link_name": "_Test Customer"
})
address.save()
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
"GST Account",
fields=["cgst_account", "sgst_account", "igst_account"],
filters = {"company": "_Test Company"})
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
"cgst_account": "CGST - _TC",
"sgst_account": "SGST - _TC",
"igst_account": "IGST - _TC",
})
gst_settings.save()
si = create_sales_invoice(do_not_save =1, rate = '60000')
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
})
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
})
si.submit() si.submit()
@ -1928,6 +1842,214 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
def test_einvoice_submission_without_irn(self):
# init
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
country = frappe.flags.country
frappe.flags.country = 'India'
si = make_sales_invoice_for_ewaybill()
self.assertRaises(frappe.ValidationError, si.submit)
si.irn = 'test_irn'
si.submit()
# reset
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice
customer_gstin = '27AACCM7806M1Z3'
customer_gstin_dtls = {
'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
company_gstin = '27AAECE4835E1ZR'
company_gstin_dtls = {
'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
}
# set cache gstin details to avoid fetching details which will require connection to GSP servers
frappe.local.gstin_cache = {}
frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
si.items = []
si.append("items", {
"item_code": "_Test Item",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 2000,
"rate": 12,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.append("items", {
"item_code": "_Test Item 2",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
"qty": 420,
"rate": 15,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
total_item_ass_value = 0
total_item_cgst_value = 0
total_item_sgst_value = 0
total_item_igst_value = 0
total_item_value = 0
for item in einvoice['ItemList']:
total_item_ass_value += item['AssAmt']
total_item_cgst_value += item['CgstAmt']
total_item_sgst_value += item['SgstAmt']
total_item_igst_value += item['IgstAmt']
total_item_value += item['TotItemVal']
self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount'])
self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt'])
value_details = einvoice['ValDtls']
self.assertEqual(einvoice['Version'], '1.1')
self.assertEqual(value_details['AssVal'], total_item_ass_value)
self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
self.assertEqual(value_details['IgstVal'], total_item_igst_value)
self.assertEqual(
value_details['TotInvVal'],
value_details['AssVal'] + value_details['CgstVal']
+ value_details['SgstVal'] + value_details['IgstVal']
+ value_details['OthChrg'] - value_details['Discount']
)
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls'])
def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Address for Eway bill",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+910000000000",
"gstin": "27AAECE4835E1ZR",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "401108"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_title": "_Test Customer-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+910000000000",
"gstin": "27AACCM7806M1Z3",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "410038"
}).insert()
address.append("links", {
"link_doctype": "Customer",
"link_name": "_Test Customer"
})
address.save()
def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({
"doctype": "Supplier",
"supplier_name": "_Test Transporter",
"country": "India",
"supplier_group": "_Test Supplier Group",
"supplier_type": "Company",
"is_transporter": 1
}).insert()
def make_sales_invoice_for_ewaybill():
make_test_address_for_ewaybill()
make_test_transporter_for_ewaybill()
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
"GST Account",
fields=["cgst_account", "sgst_account", "igst_account"],
filters = {"company": "_Test Company"}
)
if not gst_account:
gst_settings.append("gst_accounts", {
"company": "_Test Company",
"cgst_account": "CGST - _TC",
"sgst_account": "SGST - _TC",
"igst_account": "IGST - _TC",
})
gst_settings.save()
si = create_sales_invoice(do_not_save=1, rate='60000')
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"
si.mode_of_transport = 'Road'
si.transporter = '_Test Transporter'
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "CGST - _TC",
"cost_center": "Main - _TC",
"description": "CGST @ 9.0",
"rate": 9
})
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "SGST - _TC",
"cost_center": "Main - _TC",
"description": "SGST @ 9.0",
"rate": 9
})
return si
def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry` from `tabGL Entry`

View File

@ -5,15 +5,11 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now
from frappe import _ from frappe import _
from erpnext.accounts.utils import get_stock_and_account_balance
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
class ClosedAccountingPeriod(frappe.ValidationError): pass class ClosedAccountingPeriod(frappe.ValidationError): pass
class StockAccountInvalidTransaction(frappe.ValidationError): pass
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
if gl_map: if gl_map:
@ -131,10 +127,6 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
for entry in gl_map: for entry in gl_map:
make_entry(entry, adv_adj, update_outstanding, from_repost) make_entry(entry, adv_adj, update_outstanding, from_repost)
if not from_repost:
validate_account_for_perpetual_inventory(gl_map)
def make_entry(args, adv_adj, update_outstanding, from_repost=False): def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle = frappe.new_doc("GL Entry") gle = frappe.new_doc("GL Entry")
gle.update(args) gle.update(args)
@ -144,63 +136,9 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost) gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
gle.submit() gle.submit()
# check against budget
if not from_repost: if not from_repost:
validate_expense_against_budget(args) validate_expense_against_budget(args)
def validate_account_for_perpetual_inventory(gl_map):
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
account_list = [gl_entries.account for gl_entries in gl_map]
aii_accounts = [d.name for d in frappe.get_all("Account",
filters={'account_type': 'Stock', 'is_group': 0, 'company': gl_map[0].company})]
for account in account_list:
if account not in aii_accounts:
continue
# Always use current date to get stock and account balance as there can future entries for
# other items
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
gl_map[0].posting_date, gl_map[0].company)
if gl_map[0].voucher_type=="Journal Entry":
# In case of Journal Entry, there are no corresponding SL entries,
# hence deducting currency amount
account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit)
if account_bal == stock_bal:
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(account), StockAccountInvalidTransaction)
elif abs(account_bal - stock_bal) > 0.1:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
diff = flt(stock_bal - account_bal, precision)
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses on {3}.").format(
stock_bal, account_bal, frappe.bold(account), gl_map[0].posting_date)
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency')
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
journal_entry_args = {
'accounts':[
{'account': account, db_or_cr_warehouse_account : abs(diff)},
{'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff)}
]
}
frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
raise_exception=StockValueAndAccountBalanceOutOfSync,
title=_('Values Out Of Sync'),
primary_action={
'label': _('Make Journal Entry'),
'client_action': 'erpnext.route_to_adjustment_jv',
'args': journal_entry_args
})
def validate_cwip_accounts(gl_map): def validate_cwip_accounts(gl_map):
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])

View File

@ -0,0 +1,162 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
<div class="print-heading">
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
</div>
</div>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<h5 class="font-bold" style="margin-left: 15px; margin-top: 0px;">1. Transaction Details</h5>
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. No</label></div>
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. Date</label></div>
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Category</label></div>
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Type</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
</div>
<div class="col-xs-4 column-break">
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<h5 class="font-bold" style="margin-left: 15px; margin-bottom: 0px;">2. Party Details</h5>
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
<p>{{ seller.Gstin }}</p>
<p>{{ seller.LglNm }}</p>
<p>{{ seller.Addr1 }}</p>
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
<p>{{ seller.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipping</h5>
<p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p>
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
<p>{{ shipping.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
{% endif %}
</div>
{%- set buyer = einvoice.BuyerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Buyer</h5>
<p>{{ buyer.Gstin }}</p>
<p>{{ buyer.LglNm }}</p>
<p>{{ buyer.Addr1 }}</p>
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left" style="width: 3%;">Sr. No.</th>
<th class="text-left">Item</th>
<th class="text-left" style="width: 10%;">HSN Code</th>
<th class="text-left" style="width: 5%;">Qty</th>
<th class="text-left" style="width: 5%;">UOM</th>
<th class="text-left">Rate</th>
<th class="text-left" style="width: 5%;">Discount</th>
<th class="text-left">Taxable Amount</th>
<th class="text-left" style="width: 7%;">Tax Rate</th>
<th class="text-left" style="width: 5%;">Other Charges</th>
<th class="text-left">Total</th>
</tr>
</thead>
<tbody>
{% for item in einvoice.ItemList %}
<tr>
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
<td class="text-left">{{ item.PrdDesc }}</td>
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left">Taxable Amount</th>
<th class="text-left">CGST</th>
<th class="text-left"">SGST</th>
<th class="text-left">IGST</th>
<th class="text-left">CESS</th>
<th class="text-left" style="width: 10%;">State CESS</th>
<th class="text-left">Discount</th>
<th class="text-left" style="width: 10%;">Other Charges</th>
<th class="text-left" style="width: 10%;">Round Off</th>
<th class="text-left">Total Value</th>
</tr>
</thead>
<tbody>
{%- set value_details = einvoice.ValDtls -%}
<tr>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,24 @@
{
"align_labels_right": 1,
"creation": "2020-10-10 18:01:21.032914",
"custom_format": 0,
"default_print_language": "en-US",
"disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "",
"idx": 0,
"line_breaks": 1,
"modified": "2020-10-23 19:54:40.634936",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST E-Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 1,
"standard": "Yes"
}

View File

@ -47,21 +47,22 @@ def get_data(filters):
for d in gl_entries: for d in gl_entries:
asset_data = assets_details.get(d.against_voucher) asset_data = assets_details.get(d.against_voucher)
if not asset_data.get("accumulated_depreciation_amount"): if asset_data:
asset_data.accumulated_depreciation_amount = d.debit if not asset_data.get("accumulated_depreciation_amount"):
else: asset_data.accumulated_depreciation_amount = d.debit
asset_data.accumulated_depreciation_amount += d.debit else:
asset_data.accumulated_depreciation_amount += d.debit
row = frappe._dict(asset_data) row = frappe._dict(asset_data)
row.update({ row.update({
"depreciation_amount": d.debit, "depreciation_amount": d.debit,
"depreciation_date": d.posting_date, "depreciation_date": d.posting_date,
"amount_after_depreciation": (flt(row.gross_purchase_amount) - "amount_after_depreciation": (flt(row.gross_purchase_amount) -
flt(row.accumulated_depreciation_amount)), flt(row.accumulated_depreciation_amount)),
"depreciation_entry": d.voucher_no "depreciation_entry": d.voucher_no
}) })
data.append(row) data.append(row)
return data return data

View File

@ -12,11 +12,12 @@ from frappe.utils import formatdate, get_number_format_info
from six import iteritems from six import iteritems
# imported to enable erpnext.accounts.utils.get_account_currency # imported to enable erpnext.accounts.utils.get_account_currency
from erpnext.accounts.doctype.account.account import get_account_currency from erpnext.accounts.doctype.account.account import get_account_currency
from frappe.model.meta import get_field_precision
from erpnext.stock.utils import get_stock_value_on from erpnext.stock.utils import get_stock_value_on
from erpnext.stock import get_warehouse_account_map from erpnext.stock import get_warehouse_account_map
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
class FiscalYearError(frappe.ValidationError): pass class FiscalYearError(frappe.ValidationError): pass
@frappe.whitelist() @frappe.whitelist()
@ -585,24 +586,6 @@ def fix_total_debit_credit():
(dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr), (dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr),
(d.diff, d.voucher_type, d.voucher_no)) (d.diff, d.voucher_type, d.voucher_no))
def get_stock_and_account_balance(account=None, posting_date=None, company=None):
if not posting_date: posting_date = nowdate()
warehouse_account = get_warehouse_account_map(company)
account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
related_warehouses = [wh for wh, wh_details in warehouse_account.items()
if wh_details.account == account and not wh_details.is_group]
total_stock_value = 0.0
for warehouse in related_warehouses:
value = get_stock_value_on(warehouse, posting_date)
total_stock_value += value
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
def get_currency_precision(): def get_currency_precision():
precision = cint(frappe.db.get_default("currency_precision")) precision = cint(frappe.db.get_default("currency_precision"))
if not precision: if not precision:
@ -903,12 +886,6 @@ def get_coa(doctype, parent, is_root, chart=None):
return accounts return accounts
def get_stock_accounts(company):
return frappe.get_all("Account", filters = {
"account_type": "Stock",
"company": company
})
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):
def _delete_gl_entries(voucher_type, voucher_no): def _delete_gl_entries(voucher_type, voucher_no):
@ -984,3 +961,89 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle):
matched = False matched = False
break break
return matched return matched
def check_if_stock_and_account_balance_synced(posting_date, company, voucher_type=None, voucher_no=None):
if not cint(erpnext.is_perpetual_inventory_enabled(company)):
return
accounts = get_stock_accounts(company, voucher_type, voucher_no)
stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account")
for account in accounts:
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
posting_date, company)
if abs(account_bal - stock_bal) > 0.1:
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
currency=frappe.get_cached_value('Company', company, "default_currency"))
diff = flt(stock_bal - account_bal, precision)
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format(
stock_bal, account_bal, frappe.bold(account), posting_date)
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\
.format(frappe.bold(diff), frappe.bold(posting_date))
frappe.msgprint(
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
raise_exception=StockValueAndAccountBalanceOutOfSync,
title=_('Values Out Of Sync'),
primary_action={
'label': _('Make Journal Entry'),
'client_action': 'erpnext.route_to_adjustment_jv',
'args': get_journal_entry(account, stock_adjustment_account, diff)
})
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
stock_accounts = [d.name for d in frappe.db.get_all("Account", {
"account_type": "Stock",
"company": company,
"is_group": 0
})]
if voucher_type and voucher_no:
if voucher_type == "Journal Entry":
stock_accounts = [d.account for d in frappe.db.get_all("Journal Entry Account", {
"parent": voucher_no,
"account": ["in", stock_accounts]
}, "account")]
else:
stock_accounts = [d.account for d in frappe.db.get_all("GL Entry", {
"voucher_type": voucher_type,
"voucher_no": voucher_no,
"account": ["in", stock_accounts]
}, "account")]
return stock_accounts
def get_stock_and_account_balance(account=None, posting_date=None, company=None):
if not posting_date: posting_date = nowdate()
warehouse_account = get_warehouse_account_map(company)
account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
related_warehouses = [wh for wh, wh_details in warehouse_account.items()
if wh_details.account == account and not wh_details.is_group]
total_stock_value = 0.0
for warehouse in related_warehouses:
value = get_stock_value_on(warehouse, posting_date)
total_stock_value += value
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
def get_journal_entry(account, stock_adjustment_account, amount):
db_or_cr_warehouse_account =('credit_in_account_currency' if amount < 0 else 'debit_in_account_currency')
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if amount < 0 else 'credit_in_account_currency')
return {
'accounts':[{
'account': account,
db_or_cr_warehouse_account: abs(amount)
}, {
'account': stock_adjustment_account,
db_or_cr_stock_adjustment_account : abs(amount)
}]
}

View File

@ -21,9 +21,6 @@ class AssetValueAdjustment(Document):
self.reschedule_depreciations(self.new_asset_value) self.reschedule_depreciations(self.new_asset_value)
def on_cancel(self): def on_cancel(self):
if self.journal_entry:
frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
self.reschedule_depreciations(self.current_asset_value) self.reschedule_depreciations(self.current_asset_value)
def validate_date(self): def validate_date(self):

View File

@ -75,62 +75,70 @@ frappe.query_reports["Purchase Analytics"] = {
return Object.assign(options, { return Object.assign(options, {
checkboxColumn: true, checkboxColumn: true,
events: { events: {
onCheckRow: function(data) { onCheckRow: function (data) {
if (!data) return;
const data_doctype = $(
data[2].html
)[0].attributes.getNamedItem("data-doctype").value;
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
row_name = data[2].content; row_name = data[2].content;
length = data.length; length = data.length;
var tree_type = frappe.query_report.filters[0].value; if (tree_type == "Supplier") {
row_values = data
if(tree_type == "Supplier" || tree_type == "Item") { .slice(4, length - 1)
row_values = data.slice(4,length-1).map(function (column) { .map(function (column) {
return column.content; return column.content;
}) });
} } else if (tree_type == "Item") {
else { row_values = data
row_values = data.slice(3,length-1).map(function (column) { .slice(5, length - 1)
return column.content; .map(function (column) {
}) return column.content;
});
} else {
row_values = data
.slice(3, length - 1)
.map(function (column) {
return column.content;
});
} }
entry = { entry = {
'name':row_name, name: row_name,
'values':row_values values: row_values,
} };
let raw_data = frappe.query_report.chart.data; let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets; let new_datasets = raw_data.datasets;
var found = false; let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
for(var i=0; i < new_datasets.length;i++){ array.splice(index, 1)
if(new_datasets[i].name == row_name){ return true
found = true;
new_datasets.splice(i,1);
break;
} }
} return false
})
if(!found){ if (!element_found) {
new_datasets.push(entry); new_datasets.push(entry);
} }
let new_data = { let new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets datasets: new_datasets,
} };
chart_options = {
setTimeout(() => { data: new_data,
frappe.query_report.chart.update(new_data) type: "line",
},500) };
frappe.query_report.render_chart(chart_options);
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
} },
}); });
} }
} }

View File

@ -110,9 +110,15 @@ class AccountsController(TransactionBase):
self.set_inter_company_account() self.set_inter_company_account()
validate_regional(self) validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != 'Material Request': if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self) apply_pricing_rule_on_transaction(self)
def before_cancel(self):
validate_einvoice_fields(self)
def validate_deferred_start_and_end_date(self): def validate_deferred_start_and_end_date(self):
for d in self.items: for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
@ -1518,3 +1524,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
@erpnext.allow_regional @erpnext.allow_regional
def validate_regional(doc): def validate_regional(doc):
pass pass
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass

View File

@ -241,7 +241,7 @@ class BuyingController(StockController):
if rate > 0: if rate > 0:
d.rate = rate d.rate = rate
d.amount = flt(d.consumed_qty) * flt(d.rate) d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
supplied_items_cost += flt(d.amount) supplied_items_cost += flt(d.amount)
return supplied_items_cost return supplied_items_cost

View File

@ -204,21 +204,25 @@ 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 = frappe.scrub(child_doctype) if doctype == "Purchase Receipt" else "dn_detail" reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
fields = [ fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype), "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype) "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
] ]
if doctype == "Purchase Receipt": if doctype in ("Purchase Receipt", "Purchase Invoice"):
fields += [ fields += [
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype), "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype), "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype)
"sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)
] ]
if doctype == "Purchase Receipt":
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
data = frappe.db.get_list(doctype, data = frappe.db.get_list(doctype,
fields = fields, fields = fields,
filters = [ filters = [
@ -231,6 +235,7 @@ def get_returned_qty_map_for_row(row_name, doctype):
def make_return_doc(doctype, source_name, target_doc=None): def make_return_doc(doctype, source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
company = frappe.db.get_value("Delivery Note", source_name, "company") company = frappe.db.get_value("Delivery Note", source_name, "company")
default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return") default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return")
@ -290,6 +295,12 @@ def make_return_doc(doctype, source_name, target_doc=None):
def update_item(source_doc, target_doc, source_parent): def update_item(source_doc, target_doc, source_parent):
target_doc.qty = -1 * source_doc.qty target_doc.qty = -1 * source_doc.qty
if source_doc.serial_no:
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos))
if serial_nos:
target_doc.serial_no = '\n'.join(serial_nos)
if doctype == "Purchase Receipt": if doctype == "Purchase Receipt":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0)) target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
@ -305,16 +316,19 @@ def make_return_doc(doctype, source_name, target_doc=None):
target_doc.purchase_receipt_item = source_doc.name target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice": elif doctype == "Purchase Invoice":
target_doc.received_qty = -1 * source_doc.received_qty returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
target_doc.rejected_qty = -1 * source_doc.rejected_qty target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
target_doc.qty = -1* source_doc.qty target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
target_doc.stock_qty = -1 * source_doc.stock_qty target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order = source_doc.purchase_order
target_doc.purchase_receipt = source_doc.purchase_receipt target_doc.purchase_receipt = source_doc.purchase_receipt
target_doc.rejected_warehouse = source_doc.rejected_warehouse target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.po_detail = source_doc.po_detail target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name target_doc.purchase_invoice_item = source_doc.name
target_doc.price_list_rate = 0
elif doctype == "Delivery Note": elif doctype == "Delivery Note":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype) returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
@ -330,12 +344,17 @@ def make_return_doc(doctype, source_name, target_doc=None):
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
elif doctype == "Sales Invoice" or doctype == "POS Invoice": elif doctype == "Sales Invoice" or doctype == "POS Invoice":
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
target_doc.sales_order = source_doc.sales_order target_doc.sales_order = source_doc.sales_order
target_doc.delivery_note = source_doc.delivery_note target_doc.delivery_note = source_doc.delivery_note
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 target_doc.sales_invoice_item = source_doc.name
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
@ -407,3 +426,21 @@ def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, ite
filters["voucher_detail_no"] = reference_voucher_detail_no filters["voucher_detail_no"] = reference_voucher_detail_no
return filters return filters
def get_returned_serial_nos(child_doc, parent_doc):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
return_ref_field = frappe.scrub(child_doc.doctype)
if child_doc.doctype == "Delivery Note Item":
return_ref_field = "dn_detail"
serial_nos = []
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
filters = [[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "is_return", "=", 1],
[child_doc.doctype, return_ref_field, "=", child_doc.name], [parent_doc.doctype, "docstatus", "=", 1]]
for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
serial_nos.extend(get_serial_nos(row.serial_no))
return serial_nos

View File

@ -233,7 +233,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': p.incoming_rate 'incoming_rate': p.get("incoming_rate")
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@ -252,7 +252,7 @@ class SellingController(StockController):
'allow_zero_valuation': d.allow_zero_valuation_rate, 'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"), 'sales_invoice_item': d.get("sales_invoice_item"),
'dn_detail': d.get("dn_detail"), 'dn_detail': d.get("dn_detail"),
'incoming_rate': d.incoming_rate 'incoming_rate': d.get("incoming_rate")
})) }))
return il return il

View File

@ -6,7 +6,7 @@ import frappe, erpnext
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
from frappe import _ from frappe import _
import frappe.defaults import frappe.defaults
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock.stock_ledger import get_valuation_rate from erpnext.stock.stock_ledger import get_valuation_rate
@ -402,6 +402,14 @@ class StockController(AccountsController):
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():
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
def check_if_future_sle_exists(args): def check_if_future_sle_exists(args):
sl_entries = frappe.db.get_all("Stock Ledger Entry", sl_entries = frappe.db.get_all("Stock Ledger Entry",

View File

@ -17,6 +17,8 @@
"enable_free_follow_ups", "enable_free_follow_ups",
"max_visits", "max_visits",
"valid_days", "valid_days",
"inpatient_settings_section",
"allow_discharge_despite_unbilled_services",
"healthcare_service_items", "healthcare_service_items",
"inpatient_visit_charge_item", "inpatient_visit_charge_item",
"op_consulting_charge_item", "op_consulting_charge_item",
@ -302,11 +304,22 @@
"fieldname": "enable_free_follow_ups", "fieldname": "enable_free_follow_ups",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Free Follow-ups" "label": "Enable Free Follow-ups"
},
{
"fieldname": "inpatient_settings_section",
"fieldtype": "Section Break",
"label": "Inpatient Settings"
},
{
"default": "0",
"fieldname": "allow_discharge_despite_unbilled_services",
"fieldtype": "Check",
"label": "Allow Discharge Despite Unbilled Healthcare Services"
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-07-08 15:17:21.543218", "modified": "2021-01-04 10:19:22.329272",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Healthcare Settings", "name": "Healthcare Settings",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, json import frappe, json
from frappe import _ from frappe import _
from frappe.utils import today, now_datetime, getdate, get_datetime from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
@ -113,6 +113,7 @@ def schedule_inpatient(args):
inpatient_record.status = 'Admission Scheduled' inpatient_record.status = 'Admission Scheduled'
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
def schedule_discharge(args): def schedule_discharge(args):
discharge_order = json.loads(args) discharge_order = json.loads(args)
@ -126,16 +127,19 @@ def schedule_discharge(args):
frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status) frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status)
def set_details_from_ip_order(inpatient_record, ip_order): def set_details_from_ip_order(inpatient_record, ip_order):
for key in ip_order: for key in ip_order:
inpatient_record.set(key, ip_order[key]) inpatient_record.set(key, ip_order[key])
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child): def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
for item in encounter_child: for item in encounter_child:
table = inpatient_record.append(inpatient_record_child) table = inpatient_record.append(inpatient_record_child)
for df in table.meta.get('fields'): for df in table.meta.get('fields'):
table.set(df.fieldname, item.get(df.fieldname)) table.set(df.fieldname, item.get(df.fieldname))
def check_out_inpatient(inpatient_record): def check_out_inpatient(inpatient_record):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@ -144,54 +148,88 @@ def check_out_inpatient(inpatient_record):
inpatient_occupancy.check_out = now_datetime() inpatient_occupancy.check_out = now_datetime()
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
def discharge_patient(inpatient_record): def discharge_patient(inpatient_record):
validate_invoiced_inpatient(inpatient_record) validate_inpatient_invoicing(inpatient_record)
inpatient_record.discharge_date = today() inpatient_record.discharge_date = today()
inpatient_record.status = "Discharged" inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
def validate_invoiced_inpatient(inpatient_record):
pending_invoices = [] def validate_inpatient_invoicing(inpatient_record):
if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"):
return
pending_invoices = get_pending_invoices(inpatient_record)
if pending_invoices:
message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ")
formatted_doc_rows = ''
for doctype, docnames in pending_invoices.items():
formatted_doc_rows += """
<td>{0}</td>
<td>{1}</td>
</tr>""".format(doctype, docnames)
message += """
<table class='table'>
<thead>
<th>{0}</th>
<th>{1}</th>
</thead>
{2}
</table>
""".format(_("Healthcare Service"), _("Documents"), formatted_doc_rows)
frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True)
def get_pending_invoices(inpatient_record):
pending_invoices = {}
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
service_unit_names = False service_unit_names = False
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
if inpatient_occupancy.invoiced != 1: if not inpatient_occupancy.invoiced:
if service_unit_names: if service_unit_names:
service_unit_names += ", " + inpatient_occupancy.service_unit service_unit_names += ", " + inpatient_occupancy.service_unit
else: else:
service_unit_names = inpatient_occupancy.service_unit service_unit_names = inpatient_occupancy.service_unit
if service_unit_names: if service_unit_names:
pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")") pending_invoices["Inpatient Occupancy"] = service_unit_names
docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"] docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
for doc in docs: for doc in docs:
doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record) doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record)
if doc_name_list: if doc_name_list:
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices) pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
if pending_invoices: return pending_invoices
frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
.join(pending_invoices)), title=_('Unbilled Invoices'))
def get_pending_doc(doc, doc_name_list, pending_invoices): def get_pending_doc(doc, doc_name_list, pending_invoices):
if doc_name_list: if doc_name_list:
doc_ids = False doc_ids = False
for doc_name in doc_name_list: for doc_name in doc_name_list:
doc_link = get_link_to_form(doc, doc_name.name)
if doc_ids: if doc_ids:
doc_ids += ", "+doc_name.name doc_ids += ", " + doc_link
else: else:
doc_ids = doc_name.name doc_ids = doc_link
if doc_ids: if doc_ids:
pending_invoices.append(doc + " (" + doc_ids + ")") pending_invoices[doc] = doc_ids
return pending_invoices return pending_invoices
def get_inpatient_docs_not_invoiced(doc, inpatient_record):
def get_unbilled_inpatient_docs(doc, inpatient_record):
return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient, return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0}) 'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None): def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
inpatient_record.admitted_datetime = check_in inpatient_record.admitted_datetime = check_in
inpatient_record.status = 'Admitted' inpatient_record.status = 'Admitted'
@ -203,6 +241,7 @@ def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=N
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted') frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted')
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name) frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
def transfer_patient(inpatient_record, service_unit, check_in): def transfer_patient(inpatient_record, service_unit, check_in):
item_line = inpatient_record.append('inpatient_occupancies', {}) item_line = inpatient_record.append('inpatient_occupancies', {})
item_line.service_unit = service_unit item_line.service_unit = service_unit
@ -212,6 +251,7 @@ def transfer_patient(inpatient_record, service_unit, check_in):
frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied") frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
def patient_leave_service_unit(inpatient_record, check_out, leave_from): def patient_leave_service_unit(inpatient_record, check_out, leave_from):
if inpatient_record.inpatient_occupancies: if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies: for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@ -221,6 +261,7 @@ def patient_leave_service_unit(inpatient_record, check_out, leave_from):
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant") frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
inpatient_record.save(ignore_permissions = True) inpatient_record.save(ignore_permissions = True)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_leave_from(doctype, txt, searchfield, start, page_len, filters): def get_leave_from(doctype, txt, searchfield, start, page_len, filters):

View File

@ -40,6 +40,31 @@ class TestInpatientRecord(unittest.TestCase):
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status")) self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
def test_allow_discharge_despite_unbilled_services(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
setup_inpatient_settings()
patient = create_patient()
# Schedule Admission
ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True)
# Admit
service_unit = get_healthcare_service_unit()
admit_patient(ip_record, service_unit, now_datetime())
# Discharge
schedule_discharge(frappe.as_json({"patient": patient}))
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
# Should not validate Pending Invoices
ip_record.discharge()
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
def test_validate_overlap_admission(self): def test_validate_overlap_admission(self):
frappe.db.sql("""delete from `tabInpatient Record`""") frappe.db.sql("""delete from `tabInpatient Record`""")
patient = create_patient() patient = create_patient()
@ -63,6 +88,13 @@ def mark_invoiced_inpatient_occupancy(ip_record):
inpatient_occupancy.invoiced = 1 inpatient_occupancy.invoiced = 1
ip_record.save(ignore_permissions = True) ip_record.save(ignore_permissions = True)
def setup_inpatient_settings():
settings = frappe.get_single("Healthcare Settings")
settings.allow_discharge_despite_unbilled_services = 1
settings.save()
def create_inpatient(patient): def create_inpatient(patient):
patient_obj = frappe.get_doc('Patient', patient) patient_obj = frappe.get_doc('Patient', patient)
inpatient_record = frappe.new_doc('Inpatient Record') inpatient_record = frappe.new_doc('Inpatient Record')
@ -78,6 +110,7 @@ def create_inpatient(patient):
inpatient_record.scheduled_date = today() inpatient_record.scheduled_date = today()
return inpatient_record return inpatient_record
def get_healthcare_service_unit(): def get_healthcare_service_unit():
service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1}) service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
if not service_unit: if not service_unit:
@ -105,6 +138,7 @@ def get_healthcare_service_unit():
return service_unit.name return service_unit.name
return service_unit return service_unit
def get_service_unit_type(): def get_service_unit_type():
service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1}) service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1})
@ -116,6 +150,7 @@ def get_service_unit_type():
return service_unit_type.name return service_unit_type.name
return service_unit_type return service_unit_type
def create_patient(): def create_patient():
patient = frappe.db.exists('Patient', '_Test IPD Patient') patient = frappe.db.exists('Patient', '_Test IPD Patient')
if not patient: if not patient:

View File

@ -22,6 +22,7 @@ frappe.ui.form.on('Patient Appointment', {
filters: {'status': 'Active'} filters: {'status': 'Active'}
}; };
}); });
frm.set_query('practitioner', function() { frm.set_query('practitioner', function() {
return { return {
filters: { filters: {
@ -29,6 +30,7 @@ frappe.ui.form.on('Patient Appointment', {
} }
}; };
}); });
frm.set_query('service_unit', function(){ frm.set_query('service_unit', function(){
return { return {
filters: { filters: {
@ -39,6 +41,16 @@ frappe.ui.form.on('Patient Appointment', {
}; };
}); });
frm.set_query('therapy_plan', function() {
return {
filters: {
'patient': frm.doc.patient
}
};
});
frm.trigger('set_therapy_type_filter');
if (frm.is_new()) { if (frm.is_new()) {
frm.page.set_primary_action(__('Check Availability'), function() { frm.page.set_primary_action(__('Check Availability'), function() {
if (!frm.doc.patient) { if (!frm.doc.patient) {
@ -136,6 +148,24 @@ frappe.ui.form.on('Patient Appointment', {
} }
}, },
therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter');
},
set_therapy_type_filter: function(frm) {
if (frm.doc.therapy_plan) {
frm.call('get_therapy_types').then(r => {
frm.set_query('therapy_type', function() {
return {
filters: {
'name': ['in', r.message]
}
};
});
});
}
},
therapy_type: function(frm) { therapy_type: function(frm) {
if (frm.doc.therapy_type) { if (frm.doc.therapy_type) {
frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => { frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => {

View File

@ -23,9 +23,9 @@
"procedure_template", "procedure_template",
"get_procedure_from_encounter", "get_procedure_from_encounter",
"procedure_prescription", "procedure_prescription",
"therapy_plan",
"therapy_type", "therapy_type",
"get_prescribed_therapies", "get_prescribed_therapies",
"therapy_plan",
"practitioner", "practitioner",
"practitioner_name", "practitioner_name",
"department", "department",
@ -284,7 +284,7 @@
"report_hide": 1 "report_hide": 1
}, },
{ {
"depends_on": "eval:doc.patient;", "depends_on": "eval:doc.patient && doc.therapy_plan;",
"fieldname": "therapy_type", "fieldname": "therapy_type",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Therapy", "label": "Therapy",
@ -292,17 +292,16 @@
"set_only_once": 1 "set_only_once": 1
}, },
{ {
"depends_on": "eval:doc.patient && doc.__islocal;", "depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;",
"fieldname": "get_prescribed_therapies", "fieldname": "get_prescribed_therapies",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Get Prescribed Therapies" "label": "Get Prescribed Therapies"
}, },
{ {
"depends_on": "eval: doc.patient && doc.therapy_type", "depends_on": "eval: doc.patient;",
"fieldname": "therapy_plan", "fieldname": "therapy_plan",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Therapy Plan", "label": "Therapy Plan",
"mandatory_depends_on": "eval: doc.patient && doc.therapy_type",
"options": "Therapy Plan" "options": "Therapy Plan"
}, },
{ {
@ -348,7 +347,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-05-21 03:04:21.400893", "modified": "2020-12-16 13:16:58.578503",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Healthcare", "module": "Healthcare",
"name": "Patient Appointment", "name": "Patient Appointment",

View File

@ -91,6 +91,17 @@ class PatientAppointment(Document):
if fee_validity: if fee_validity:
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till)) frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
def get_therapy_types(self):
if not self.therapy_plan:
return
therapy_types = []
doc = frappe.get_doc('Therapy Plan', self.therapy_plan)
for entry in doc.therapy_plan_details:
therapy_types.append(entry.therapy_type)
return therapy_types
@frappe.whitelist() @frappe.whitelist()
def check_payment_fields_reqd(patient): def check_payment_fields_reqd(patient):
@ -145,7 +156,7 @@ def invoice_appointment(appointment_doc):
sales_invoice.flags.ignore_mandatory = True sales_invoice.flags.ignore_mandatory = True
sales_invoice.save(ignore_permissions=True) sales_invoice.save(ignore_permissions=True)
sales_invoice.submit() sales_invoice.submit()
frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True) frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name) frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)

View File

@ -397,7 +397,8 @@ regional_overrides = {
'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.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',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields'
}, },
'United Arab Emirates': { 'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',

View File

@ -813,7 +813,7 @@
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-10-16 15:02:04.283657", "modified": "2021-01-01 16:54:33.477439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",
@ -855,7 +855,6 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"search_fields": "employee_name", "search_fields": "employee_name",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",

View File

@ -135,7 +135,7 @@ class Employee(NestedSet):
try: try:
frappe.get_doc({ frappe.get_doc({
"doctype": "File", "doctype": "File",
"file_name": self.image, "file_url": self.image,
"attached_to_doctype": "User", "attached_to_doctype": "User",
"attached_to_name": self.user_id "attached_to_name": self.user_id
}).insert() }).insert()

View File

@ -11,6 +11,7 @@
"employee", "employee",
"employee_name", "employee_name",
"department", "department",
"company",
"column_break1", "column_break1",
"leave_type", "leave_type",
"from_date", "from_date",
@ -219,6 +220,15 @@
"label": "Leave Policy Assignment", "label": "Leave Policy Assignment",
"options": "Leave Policy Assignment", "options": "Leave Policy Assignment",
"read_only": 1 "read_only": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
} }
], ],
"icon": "fa fa-ok", "icon": "fa fa-ok",
@ -226,7 +236,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-20 14:25:10.314323", "modified": "2021-01-04 18:46:13.184104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Allocation", "name": "Leave Allocation",

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2019-05-09 15:47:39.760406", "creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
@ -8,6 +9,7 @@
"leave_type", "leave_type",
"transaction_type", "transaction_type",
"transaction_name", "transaction_name",
"company",
"leaves", "leaves",
"column_break_7", "column_break_7",
"from_date", "from_date",
@ -106,12 +108,22 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Holiday List", "label": "Holiday List",
"options": "Holiday List" "options": "Holiday List"
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
} }
], ],
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"modified": "2020-09-04 12:16:36.569066", "links": [],
"modified": "2021-01-04 18:47:45.146652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Ledger Entry", "name": "Leave Ledger Entry",

View File

@ -4,22 +4,10 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'leave_policy', 'fieldname': 'leave_policy',
'non_standard_fieldnames': {
'Employee Grade': 'default_leave_policy'
},
'transactions': [ 'transactions': [
{
'label': _('Employees'),
'items': ['Employee', 'Employee Grade']
},
{ {
'label': _('Leaves'), 'label': _('Leaves'),
'items': ['Leave Allocation'] 'items': ['Leave Allocation']
}, },
] ]
} }

View File

@ -111,13 +111,14 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-15 15:18:15.227848", "modified": "2020-12-31 16:43:30.695206",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Policy Assignment", "name": "Leave Policy Assignment",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@ -127,9 +128,11 @@
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@ -139,9 +142,11 @@
"report": 1, "report": 1,
"role": "HR User", "role": "HR User",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@ -151,6 +156,7 @@
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import frappe, math, json import frappe, math, json
import erpnext import erpnext
from frappe import _ from frappe import _
from six import string_types
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
@ -280,10 +281,13 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict
return write_off return write_off
@frappe.whitelist() @frappe.whitelist()
def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0): def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0):
# if loan is passed it will be considered as full unpledge # if no security_map is passed it will be considered as full unpledge
if security_map and isinstance(security_map, string_types):
security_map = json.loads(security_map)
if loan: if loan:
pledge_qty_map = get_pledged_security_qty(loan) pledge_qty_map = security_map or get_pledged_security_qty(loan)
loan_doc = frappe.get_doc('Loan', loan) loan_doc = frappe.get_doc('Loan', loan)
unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company, unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company,
loan_doc.applicant_type, loan_doc.applicant) loan_doc.applicant_type, loan_doc.applicant)

View File

@ -45,7 +45,7 @@ class TestLoan(unittest.TestCase):
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
self.applicant1 = make_employee("robert_loan@loan.com") self.applicant1 = make_employee("robert_loan@loan.com")
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR') make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company")
if not frappe.db.exists("Customer", "_Test Loan Customer"): if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
@ -325,6 +325,64 @@ class TestLoan(unittest.TestCase):
self.assertEquals(amounts['payable_principal_amount'], 0.0) self.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0) self.assertEqual(amounts['interest_amount'], 0)
def test_partial_loan_security_unpledge(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 2000.00
},
{
"loan_security": "Test Security 2",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000)
repayment_entry.submit()
unpledge_map = {'Test Security 2': 2000}
unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
unpledge_request.submit()
unpledge_request.status = 'Approved'
unpledge_request.save()
unpledge_request.submit()
unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1)
def test_santined_loan_security_unpledge(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
unpledge_map = {'Test Security 1': 4000}
unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
unpledge_request.submit()
unpledge_request.status = 'Approved'
unpledge_request.save()
unpledge_request.submit()
def test_disbursal_check_with_shortfall(self): def test_disbursal_check_with_shortfall(self):
pledges = [{ pledges = [{
"loan_security": "Test Security 2", "loan_security": "Test Security 2",

View File

@ -81,7 +81,6 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
process_loan_security_shortfall) process_loan_security_shortfall)
def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall): def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall):
existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name") existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
if existing_shortfall: if existing_shortfall:

View File

@ -30,6 +30,8 @@ class LoanSecurityUnpledge(Document):
d.idx, frappe.bold(d.loan_security))) d.idx, frappe.bold(d.loan_security)))
def validate_unpledge_qty(self): def validate_unpledge_qty(self):
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import get_ltv_ratio
pledge_qty_map = get_pledged_security_qty(self.loan) pledge_qty_map = get_pledged_security_qty(self.loan)
ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type", ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
@ -42,11 +44,19 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime()) "valid_upto": (">=", get_datetime())
}, as_list=1)) }, as_list=1))
total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid', loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable', 'written_off_amount']) 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
if loan_details.status == 'Disbursed':
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
else:
pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
security_value = 0 security_value = 0
unpledge_qty_map = {}
ltv_ratio = 0
for security in self.securities: for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security, 0) pledged_qty = pledge_qty_map.get(security.loan_security, 0)
@ -57,13 +67,15 @@ class LoanSecurityUnpledge(Document):
msg += _("You are trying to unpledge more.") msg += _("You are trying to unpledge more.")
frappe.throw(msg, title=_("Loan Security Unpledge Error")) frappe.throw(msg, title=_("Loan Security Unpledge Error"))
qty_after_unpledge = pledged_qty - security.qty unpledge_qty_map.setdefault(security.loan_security, 0)
ltv_ratio = ltv_ratio_map.get(security.loan_security_type) unpledge_qty_map[security.loan_security] += security.qty
current_price = loan_security_price_map.get(security.loan_security) for security in pledge_qty_map:
if not current_price: if not ltv_ratio:
frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security))) ltv_ratio = get_ltv_ratio(security)
qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0)
current_price = loan_security_price_map.get(security)
security_value += qty_after_unpledge * current_price security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0: if not security_value and flt(pending_principal_amount, 2) > 0:

View File

@ -17,6 +17,7 @@ class OverlapError(frappe.ValidationError): pass
class OperationMismatchError(frappe.ValidationError): pass class OperationMismatchError(frappe.ValidationError): pass
class OperationSequenceError(frappe.ValidationError): pass class OperationSequenceError(frappe.ValidationError): pass
class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document): class JobCard(Document):
def validate(self): def validate(self):
@ -217,33 +218,49 @@ class JobCard(Document):
field = "operation_id" field = "operation_id"
data = self.get_current_operation_data() data = self.get_current_operation_data()
if data and len(data) > 0: if data and len(data) > 0:
for_quantity = data[0].completed_qty for_quantity = flt(data[0].completed_qty)
time_in_mins = data[0].time_in_mins time_in_mins = flt(data[0].time_in_mins)
if self.get(field): wo = frappe.get_doc('Work Order', self.work_order)
time_data = frappe.db.sql(""" if self.operation_id:
self.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo)
def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return
if wo.produced_qty > for_quantity:
first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.")
.format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)))
second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.")
.format(frappe.bold(get_link_to_form("Work Order", self.work_order))))
frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg),
JobCardCancelError, title = _("Error"))
def update_work_order_data(self, for_quantity, time_in_mins, wo):
time_data = frappe.db.sql("""
SELECT SELECT
min(from_time) as start_time, max(to_time) as end_time min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE WHERE
jctl.parent = jc.name and jc.work_order = %s jctl.parent = jc.name and jc.work_order = %s
and jc.{0} = %s and jc.docstatus = 1 and jc.operation_id = %s and jc.docstatus = 1
""".format(field), (self.work_order, self.get(field)), as_dict=1) """, (self.work_order, self.operation_id), as_dict=1)
wo = frappe.get_doc('Work Order', self.work_order) for data in wo.operations:
if data.get("name") == self.operation_id:
data.completed_qty = for_quantity
data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None
data.actual_end_time = time_data[0].end_time if time_data else None
for data in wo.operations: wo.flags.ignore_validate_update_after_submit = True
if data.get("name") == self.get(field): wo.update_operation_status()
data.completed_qty = for_quantity wo.calculate_operating_cost()
data.actual_operation_time = time_in_mins wo.set_actual_dates()
data.actual_start_time = time_data[0].start_time if time_data else None wo.save()
data.actual_end_time = time_data[0].end_time if time_data else None
wo.flags.ignore_validate_update_after_submit = True
wo.update_operation_status()
wo.calculate_operating_cost()
wo.set_actual_dates()
wo.save()
def get_current_operation_data(self): def get_current_operation_data(self):
return frappe.get_all('Job Card', return frappe.get_all('Job Card',

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today from frappe.utils import flt, now, add_months, cint, today, add_to_date
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
@ -14,6 +14,7 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
class TestWorkOrder(unittest.TestCase): class TestWorkOrder(unittest.TestCase):
def setUp(self): def setUp(self):
@ -369,21 +370,49 @@ class TestWorkOrder(unittest.TestCase):
self.assertEqual(ste.total_additional_costs, 1000) self.assertEqual(ste.total_additional_costs, 1000)
def test_job_card(self): def test_job_card(self):
stock_entries = []
data = frappe.get_cached_value('BOM', data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data: bom, bom_item = data
frappe.db.set_value("Manufacturing Settings",
None, "disable_capacity_planning", 0)
bom, bom_item = data bom_doc = frappe.get_doc('BOM', bom)
work_order = make_wo_order_test_record(item=bom_item, qty=1,
bom_no=bom, source_warehouse="_Test Warehouse - _TC")
bom_doc = frappe.get_doc('BOM', bom) for row in work_order.required_items:
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code,
self.assertTrue(work_order.planned_end_date) target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100)
stock_entries.append(stock_entry_doc)
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
self.assertEqual(len(job_cards), len(bom_doc.operations)) ste.submit()
stock_entries.append(ste)
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
self.assertEqual(len(job_cards), len(bom_doc.operations))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", {
"from_time": now(),
"hours": i,
"to_time": add_to_date(now(), i),
"completed_qty": doc.for_quantity
})
doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
ste1.submit()
stock_entries.append(ste1)
for job_card in job_cards:
doc = frappe.get_doc("Job Card", job_card)
self.assertRaises(JobCardCancelError, doc.cancel)
stock_entries.reverse()
for stock_entry in stock_entries:
stock_entry.cancel()
def test_capcity_planning(self): def test_capcity_planning(self):
frappe.db.set_value("Manufacturing Settings", None, { frappe.db.set_value("Manufacturing Settings", None, {
@ -509,7 +538,6 @@ class TestWorkOrder(unittest.TestCase):
ste1.submit() ste1.submit()
ste_cancel_list.append(ste1) ste_cancel_list.append(ste1)
print(wo_order.name)
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2)) ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
self.assertEquals(ste3.fg_completed_qty, 2) self.assertEquals(ste3.fg_completed_qty, 2)

View File

@ -711,6 +711,7 @@ erpnext.patches.v13_0.delete_old_sales_reports
execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation") execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings")
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
erpnext.patches.v12_0.add_taxjar_integration_field erpnext.patches.v12_0.add_taxjar_integration_field
@ -732,6 +733,7 @@ erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_reason_for_resignation_in_employee
erpnext.patches.v13_0.update_custom_fields_for_shopify erpnext.patches.v13_0.update_custom_fields_for_shopify
@ -741,3 +743,4 @@ erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_l
erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.add_po_to_global_search
erpnext.patches.v13_0.update_returned_qty_in_pr_dn erpnext.patches.v13_0.update_returned_qty_in_pr_dn
erpnext.patches.v13_0.update_project_template_tasks erpnext.patches.v13_0.update_project_template_tasks
erpnext.patches.v13_0.set_company_in_leave_ledger_entry

View File

@ -0,0 +1,56 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.india.setup import add_permissions, add_print_formats
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
frappe.reload_doc("custom", "doctype", "custom_field")
frappe.reload_doc("regional", "doctype", "e_invoice_settings")
custom_fields = {
'Sales Invoice': [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
]
}
create_custom_fields(custom_fields, update=True)
add_permissions()
add_print_formats()
einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
t = {
'mode_of_transport': [{'default': None}],
'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}],
'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
'ewaybill': [
{'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'},
{'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
]
}
for field, conditions in t.items():
for c in conditions:
[(prop, value)] = c.items()
frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)

View File

@ -0,0 +1,7 @@
import frappe
def execute():
frappe.reload_doc('HR', 'doctype', 'Leave Allocation')
frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry')
frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""")
frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""")

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import nowdate from frappe.utils import nowdate, flt
from erpnext.accounts.doctype.account.test_account import create_account from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
from erpnext.loan_management.doctype.loan.loan import make_repayment_entry from erpnext.loan_management.doctype.loan.loan import make_repayment_entry
@ -113,15 +113,15 @@ def execute():
interest_paid = 0 interest_paid = 0
principal_paid = 0 principal_paid = 0
if total_interest > entry.interest_amount: if flt(total_interest) > flt(entry.interest_amount):
interest_paid = entry.interest_amount interest_paid = flt(entry.interest_amount)
else: else:
interest_paid = total_interest interest_paid = flt(total_interest)
if total_principal > entry.payable_principal_amount: if flt(total_principal) > flt(entry.payable_principal_amount):
principal_paid = entry.payable_principal_amount principal_paid = flt(entry.payable_principal_amount)
else: else:
principal_paid = total_principal principal_paid = flt(total_principal)
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` + %s, SET paid_principal_amount = `paid_principal_amount` + %s,
@ -129,8 +129,8 @@ def execute():
WHERE name = %s""", WHERE name = %s""",
(principal_paid, interest_paid, entry.name)) (principal_paid, interest_paid, entry.name))
total_principal -= principal_paid total_principal = flt(total_principal) - principal_paid
total_interest -= interest_paid total_interest = flt(total_interest) - interest_paid
def create_loan_type(loan, loan_type_name, penalty_account): def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc = frappe.new_doc('Loan Type') loan_type_doc = frappe.new_doc('Loan Type')

View File

@ -5,11 +5,11 @@ from __future__ import unicode_literals
import frappe import frappe
def execute(): def execute():
# udpate sales cycle # update sales cycle
for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']: for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d) frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d)
# udpate purchase cycle # update purchase cycle
for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']: for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d) frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d)

View File

@ -86,19 +86,21 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
self.assertEqual(declaration.total_exemption_amount, 100000) self.assertEqual(declaration.total_exemption_amount, 100000)
def create_payroll_period(): def create_payroll_period(**args):
if not frappe.db.exists("Payroll Period", "_Test Payroll Period"): args = frappe._dict(args)
name = args.name or "_Test Payroll Period"
if not frappe.db.exists("Payroll Period", name):
from datetime import date from datetime import date
payroll_period = frappe.get_doc(dict( payroll_period = frappe.get_doc(dict(
doctype = 'Payroll Period', doctype = 'Payroll Period',
name = "_Test Payroll Period", name = name,
company = erpnext.get_default_company(), company = args.company or erpnext.get_default_company(),
start_date = date(date.today().year, 1, 1), start_date = args.start_date or date(date.today().year, 1, 1),
end_date = date(date.today().year, 12, 31) end_date = args.end_date or date(date.today().year, 12, 31)
)).insert() )).insert()
return payroll_period return payroll_period
else: else:
return frappe.get_doc("Payroll Period", "_Test Payroll Period") return frappe.get_doc("Payroll Period", name)
def create_exemption_category(): def create_exemption_category():
if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"):

View File

@ -3,8 +3,11 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe #import frappe
import erpnext
from frappe.model.document import Document from frappe.model.document import Document
class IncomeTaxSlab(Document): class IncomeTaxSlab(Document):
pass def validate(self):
if self.company:
self.currency = erpnext.get_company_currency(self.company)

View File

@ -17,8 +17,7 @@
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Employee", "label": "Employee",
"options": "Employee", "options": "Employee"
"read_only": 1
}, },
{ {
"fetch_from": "employee.employee_name", "fetch_from": "employee.employee_name",
@ -52,7 +51,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-30 12:40:07.999878", "modified": "2020-12-17 15:43:29.542977",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Payroll Employee Detail", "name": "Payroll Employee Detail",

View File

@ -10,15 +10,22 @@ frappe.ui.form.on('Payroll Entry', {
} }
frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet); frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet);
frm.set_query("department", function() { frm.events.department_filters(frm);
frm.events.payroll_payable_account_filters(frm);
},
department_filters: function (frm) {
frm.set_query("department", function () {
return { return {
"filters": { "filters": {
"company": frm.doc.company, "company": frm.doc.company,
} }
}; };
}); });
},
frm.set_query("payroll_payable_account", function() { payroll_payable_account_filters: function (frm) {
frm.set_query("payroll_payable_account", function () {
return { return {
filters: { filters: {
"company": frm.doc.company, "company": frm.doc.company,
@ -29,20 +36,20 @@ frappe.ui.form.on('Payroll Entry', {
}); });
}, },
refresh: function(frm) { refresh: function (frm) {
if (frm.doc.docstatus == 0) { if (frm.doc.docstatus == 0) {
if(!frm.is_new()) { if (!frm.is_new()) {
frm.page.clear_primary_action(); frm.page.clear_primary_action();
frm.add_custom_button(__("Get Employees"), frm.add_custom_button(__("Get Employees"),
function() { function () {
frm.events.get_employee_details(frm); frm.events.get_employee_details(frm);
} }
).toggleClass('btn-primary', !(frm.doc.employees || []).length); ).toggleClass('btn-primary', !(frm.doc.employees || []).length);
} }
if ((frm.doc.employees || []).length) { if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
frm.page.clear_primary_action(); frm.page.clear_primary_action();
frm.page.set_primary_action(__('Create Salary Slips'), () => { frm.page.set_primary_action(__('Create Salary Slips'), () => {
frm.save('Submit').then(()=>{ frm.save('Submit').then(() => {
frm.page.clear_primary_action(); frm.page.clear_primary_action();
frm.refresh(); frm.refresh();
frm.events.refresh(frm); frm.events.refresh(frm);
@ -61,48 +68,48 @@ frappe.ui.form.on('Payroll Entry', {
doc: frm.doc, doc: frm.doc,
method: 'fill_employee_details', method: 'fill_employee_details',
}).then(r => { }).then(r => {
if (r.docs && r.docs[0].employees){ if (r.docs && r.docs[0].employees) {
frm.employees = r.docs[0].employees; frm.employees = r.docs[0].employees;
frm.dirty(); frm.dirty();
frm.save(); frm.save();
frm.refresh(); frm.refresh();
if(r.docs[0].validate_attendance){ if (r.docs[0].validate_attendance) {
render_employee_attendance(frm, r.message); render_employee_attendance(frm, r.message);
} }
} }
}) });
}, },
create_salary_slips: function(frm) { create_salary_slips: function (frm) {
frm.call({ frm.call({
doc: frm.doc, doc: frm.doc,
method: "create_salary_slips", method: "create_salary_slips",
callback: function(r) { callback: function () {
frm.refresh(); frm.refresh();
frm.toolbar.refresh(); frm.toolbar.refresh();
} }
}) });
}, },
add_context_buttons: function(frm) { add_context_buttons: function (frm) {
if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) { if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
frm.events.add_bank_entry_button(frm); frm.events.add_bank_entry_button(frm);
} else if(frm.doc.salary_slips_created) { } else if (frm.doc.salary_slips_created) {
frm.add_custom_button(__("Submit Salary Slip"), function() { frm.add_custom_button(__("Submit Salary Slip"), function () {
submit_salary_slip(frm); submit_salary_slip(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
}, },
add_bank_entry_button: function(frm) { add_bank_entry_button: function (frm) {
frappe.call({ frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries', method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries',
args: { args: {
'name': frm.doc.name 'name': frm.doc.name
}, },
callback: function(r) { callback: function (r) {
if (r.message && !r.message.submitted) { if (r.message && !r.message.submitted) {
frm.add_custom_button("Make Bank Entry", function() { frm.add_custom_button("Make Bank Entry", function () {
make_bank_entry(frm); make_bank_entry(frm);
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
@ -141,8 +148,37 @@ frappe.ui.form.on('Payroll Entry', {
}, },
payroll_frequency: function (frm) { payroll_frequency: function (frm) {
frm.trigger("set_start_end_dates"); frm.trigger("set_start_end_dates").then( ()=> {
frm.events.clear_employee_table(frm); frm.events.clear_employee_table(frm);
frm.events.get_employee_with_salary_slip_and_set_query(frm);
});
},
employee_filters: function (frm, emp_list) {
frm.set_query('employee', 'employees', () => {
return {
filters: {
name: ["not in", emp_list]
}
};
});
},
get_employee_with_salary_slip_and_set_query: function (frm) {
frappe.db.get_list('Salary Slip', {
filters: {
start_date: frm.doc.start_date,
end_date: frm.doc.end_date,
docstatus: 1,
},
fields: ['employee']
}).then((emp) => {
var emp_list = [];
emp.forEach((employee_data) => {
emp_list.push(Object.values(employee_data)[0]);
});
frm.events.employee_filters(frm, emp_list);
});
}, },
company: function (frm) { company: function (frm) {
@ -164,17 +200,17 @@ frappe.ui.form.on('Payroll Entry', {
from_currency: frm.doc.currency, from_currency: frm.doc.currency,
to_currency: company_currency, to_currency: company_currency,
}, },
callback: function(r) { callback: function (r) {
frm.set_value("exchange_rate", flt(r.message)); frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0); frm.set_df_property('exchange_rate', 'hidden', 0);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency +
+ " = [?] " + company_currency); " = [?] " + company_currency);
} }
}); });
} else { } else {
frm.set_value("exchange_rate", 1.0); frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1); frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" ); frm.set_df_property("exchange_rate", "description", "");
} }
} }
}, },
@ -192,9 +228,9 @@ frappe.ui.form.on('Payroll Entry', {
}, },
start_date: function (frm) { start_date: function (frm) {
if(!in_progress && frm.doc.start_date){ if (!in_progress && frm.doc.start_date) {
frm.trigger("set_end_date"); frm.trigger("set_end_date");
}else{ } else {
// reset flag // reset flag
in_progress = false; in_progress = false;
} }
@ -228,7 +264,7 @@ frappe.ui.form.on('Payroll Entry', {
} }
}, },
set_end_date: function(frm){ set_end_date: function (frm) {
frappe.call({ frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date', method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
args: { args: {
@ -243,19 +279,19 @@ frappe.ui.form.on('Payroll Entry', {
}); });
}, },
validate_attendance: function(frm){ validate_attendance: function (frm) {
if(frm.doc.validate_attendance && frm.doc.employees){ if (frm.doc.validate_attendance && frm.doc.employees) {
frappe.call({ frappe.call({
method: 'validate_employee_attendance', method: 'validate_employee_attendance',
args: {}, args: {},
callback: function(r) { callback: function (r) {
render_employee_attendance(frm, r.message); render_employee_attendance(frm, r.message);
}, },
doc: frm.doc, doc: frm.doc,
freeze: true, freeze: true,
freeze_message: __('Validating Employee Attendance...') freeze_message: __('Validating Employee Attendance...')
}); });
}else{ } else {
frm.fields_dict.attendance_detail_html.html(""); frm.fields_dict.attendance_detail_html.html("");
} }
}, },
@ -270,18 +306,20 @@ frappe.ui.form.on('Payroll Entry', {
const submit_salary_slip = function (frm) { const submit_salary_slip = function (frm) {
frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'), frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'),
function() { function () {
frappe.call({ frappe.call({
method: 'submit_salary_slips', method: 'submit_salary_slips',
args: {}, args: {},
callback: function() {frm.events.refresh(frm);}, callback: function () {
frm.events.refresh(frm);
},
doc: frm.doc, doc: frm.doc,
freeze: true, freeze: true,
freeze_message: __('Submitting Salary Slips and creating Journal Entry...') freeze_message: __('Submitting Salary Slips and creating Journal Entry...')
}); });
}, },
function() { function () {
if(frappe.dom.freeze_count) { if (frappe.dom.freeze_count) {
frappe.dom.unfreeze(); frappe.dom.unfreeze();
frm.events.refresh(frm); frm.events.refresh(frm);
} }
@ -295,9 +333,11 @@ let make_bank_entry = function (frm) {
return frappe.call({ return frappe.call({
doc: cur_frm.doc, doc: cur_frm.doc,
method: "make_payment_entry", method: "make_payment_entry",
callback: function() { callback: function () {
frappe.set_route( frappe.set_route(
'List', 'Journal Entry', {"Journal Entry Account.reference_name": frm.doc.name} 'List', 'Journal Entry', {
"Journal Entry Account.reference_name": frm.doc.name
}
); );
}, },
freeze: true, freeze: true,
@ -309,11 +349,19 @@ let make_bank_entry = function (frm) {
} }
}; };
let render_employee_attendance = function (frm, data) {
let render_employee_attendance = function(frm, data) {
frm.fields_dict.attendance_detail_html.html( frm.fields_dict.attendance_detail_html.html(
frappe.render_template('employees_to_mark_attendance', { frappe.render_template('employees_to_mark_attendance', {
data: data data: data
}) })
); );
} };
frappe.ui.form.on('Payroll Employee Detail', {
employee: function(frm) {
frm.events.clear_employee_table(frm);
if (!frm.doc.payroll_frequency) {
frappe.throw(__("Please set a Payroll Frequency"));
}
}
});

View File

@ -129,8 +129,7 @@
"fieldname": "employees", "fieldname": "employees",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Employee Details", "label": "Employee Details",
"options": "Payroll Employee Detail", "options": "Payroll Employee Detail"
"read_only": 1
}, },
{ {
"fieldname": "section_break_13", "fieldname": "section_break_13",
@ -290,7 +289,7 @@
"icon": "fa fa-cog", "icon": "fa fa-cog",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-23 13:00:33.753228", "modified": "2020-12-17 15:13:17.766210",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Payroll Entry", "name": "Payroll Entry",

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.model.document import Document from frappe.model.document import Document
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, date_diff, comma_and
from frappe import _ from frappe import _
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@ -19,16 +19,29 @@ class PayrollEntry(Document):
# check if salary slips were manually submitted # check if salary slips were manually submitted
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
if cint(entries) == len(self.employees): if cint(entries) == len(self.employees):
self.set_onload("submitted_ss", True) self.set_onload("submitted_ss", True)
def validate(self):
self.number_of_employees = len(self.employees)
def on_submit(self): def on_submit(self):
self.create_salary_slips() self.create_salary_slips()
def before_submit(self): def before_submit(self):
self.validate_employee_details()
if self.validate_attendance: if self.validate_attendance:
if self.validate_employee_attendance(): if self.validate_employee_attendance():
frappe.throw(_("Cannot Submit, Employees left to mark attendance")) frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
def validate_employee_details(self):
emp_with_sal_slip = []
for employee_details in self.employees:
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
emp_with_sal_slip.append(employee_details.employee)
if len(emp_with_sal_slip):
frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip)))
def on_cancel(self): def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
where payroll_entry=%s """, (self.name))) where payroll_entry=%s """, (self.name)))
@ -71,8 +84,17 @@ class PayrollEntry(Document):
and t2.docstatus = 1 and t2.docstatus = 1
%s order by t2.from_date desc %s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True) """ % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True)
emp_list = self.remove_payrolled_employees(emp_list)
return emp_list return emp_list
def remove_payrolled_employees(self, emp_list):
for employee_details in emp_list:
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
emp_list.remove(employee_details)
return emp_list
def fill_employee_details(self): def fill_employee_details(self):
self.set('employees', []) self.set('employees', [])
employees = self.get_emp_list() employees = self.get_emp_list()
@ -94,7 +116,7 @@ class PayrollEntry(Document):
for d in employees: for d in employees:
self.append('employees', d) self.append('employees', d)
self.number_of_employees = len(employees) self.number_of_employees = len(self.employees)
if self.validate_attendance: if self.validate_attendance:
return self.validate_employee_attendance() return self.validate_employee_attendance()
@ -126,8 +148,8 @@ class PayrollEntry(Document):
""" """
self.check_permission('write') self.check_permission('write')
self.created = 1 self.created = 1
emp_list = [d.employee for d in self.get_emp_list()] employees = [emp.employee for emp in self.employees]
if emp_list: if employees:
args = frappe._dict({ args = frappe._dict({
"salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet, "salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet,
"payroll_frequency": self.payroll_frequency, "payroll_frequency": self.payroll_frequency,
@ -141,10 +163,10 @@ class PayrollEntry(Document):
"exchange_rate": self.exchange_rate, "exchange_rate": self.exchange_rate,
"currency": self.currency "currency": self.currency
}) })
if len(emp_list) > 30: if len(employees) > 30:
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args) frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
else: else:
create_salary_slips_for_employees(emp_list, args, publish_progress=False) create_salary_slips_for_employees(employees, args, publish_progress=False)
# since this method is called via frm.call this doc needs to be updated manually # since this method is called via frm.call this doc needs to be updated manually
self.reload() self.reload()

View File

@ -22,7 +22,7 @@ class TestPayrollEntry(unittest.TestCase):
frappe.db.sql("delete from `tab%s`" % dt) frappe.db.sql("delete from `tab%s`" % dt)
make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_earning_salary_component(setup=True, company_list=["_Test Company"])
make_deduction_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0) frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
@ -107,9 +107,9 @@ class TestPayrollEntry(unittest.TestCase):
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC": frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
"_Test Payroll Payable - _TC") "_Test Payroll Payable - _TC")
currency=frappe.db.get_value("Company", "_Test Company", "default_currency")
make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False)
make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency")) make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
dates = get_start_end_dates('Monthly', nowdate()) dates = get_start_end_dates('Monthly', nowdate())
if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):

View File

@ -125,15 +125,15 @@ frappe.ui.form.on("Salary Slip", {
change_form_labels: function(frm, company_currency) { change_form_labels: function(frm, company_currency) {
frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
"base_net_pay", "base_rounded_total", "base_total_in_words"], "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
company_currency); company_currency);
frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"],
frm.doc.currency); frm.doc.currency);
// toggle fields // toggle fields
frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction",
"base_net_pay", "base_rounded_total", "base_total_in_words"], "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
frm.doc.currency != company_currency); frm.doc.currency != company_currency);
}, },
@ -151,7 +151,6 @@ frappe.ui.form.on("Salary Slip", {
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"]; var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false); frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false); frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false);
calculate_totals(frm);
frm.trigger("set_dynamic_labels"); frm.trigger("set_dynamic_labels");
}, },
@ -214,14 +213,16 @@ frappe.ui.form.on('Salary Slip Timesheet', {
}); });
var calculate_totals = function(frm) { var calculate_totals = function(frm) {
if (frm.doc.earnings || frm.doc.deductions) { if (frm.doc.docstatus === 0) {
frappe.call({ if (frm.doc.earnings || frm.doc.deductions) {
method: "set_totals", frappe.call({
doc: frm.doc, method: "set_totals",
callback: function() { doc: frm.doc,
frm.refresh_fields(); callback: function() {
} frm.refresh_fields();
}); }
});
}
} }
}; };

View File

@ -69,9 +69,13 @@
"net_pay_info", "net_pay_info",
"net_pay", "net_pay",
"base_net_pay", "base_net_pay",
"year_to_date",
"base_year_to_date",
"column_break_53", "column_break_53",
"rounded_total", "rounded_total",
"base_rounded_total", "base_rounded_total",
"month_to_date",
"base_month_to_date",
"section_break_55", "section_break_55",
"total_in_words", "total_in_words",
"column_break_69", "column_break_69",
@ -578,13 +582,41 @@
{ {
"fieldname": "column_break_69", "fieldname": "column_break_69",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "year_to_date",
"fieldtype": "Currency",
"label": "Year To Date",
"options": "currency",
"read_only": 1
},
{
"fieldname": "month_to_date",
"fieldtype": "Currency",
"label": "Month To Date",
"options": "currency",
"read_only": 1
},
{
"fieldname": "base_year_to_date",
"fieldtype": "Currency",
"label": "Year To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "base_month_to_date",
"fieldtype": "Currency",
"label": "Month To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 9, "idx": 9,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-21 23:02:59.400249", "modified": "2020-12-21 23:43:44.959840",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Slip", "name": "Salary Slip",

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import datetime, math import datetime, math
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe import msgprint, _ from frappe import msgprint, _
@ -18,6 +18,7 @@ from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_fac
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
from erpnext.accounts.utils import get_fiscal_year
class SalarySlip(TransactionBase): class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -49,6 +50,8 @@ class SalarySlip(TransactionBase):
self.get_working_days_details(lwp = self.leave_without_pay) self.get_working_days_details(lwp = self.leave_without_pay)
self.calculate_net_pay() self.calculate_net_pay()
self.compute_year_to_date()
self.compute_month_to_date()
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
@ -140,8 +143,8 @@ class SalarySlip(TransactionBase):
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0 self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet() self.set_time_sheet()
self.pull_sal_struct() self.pull_sal_struct()
payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"]) ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1)
return [payroll_based_on, consider_unmarked_attendance_as] return [ps.payroll_based_on, ps.consider_unmarked_attendance_as]
def set_time_sheet(self): def set_time_sheet(self):
if self.salary_slip_based_on_timesheet: if self.salary_slip_based_on_timesheet:
@ -421,16 +424,19 @@ class SalarySlip(TransactionBase):
def calculate_net_pay(self): def calculate_net_pay(self):
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("earnings") self.calculate_component_amounts("earnings")
self.gross_pay = self.get_component_totals("earnings") self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay')) self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay'))
if self.salary_structure: if self.salary_structure:
self.calculate_component_amounts("deductions") self.calculate_component_amounts("deductions")
self.total_deduction = self.get_component_totals("deductions")
self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
self.set_loan_repayment() self.set_loan_repayment()
self.set_component_amounts_based_on_payment_days()
self.set_net_pay()
def set_net_pay(self):
self.total_deduction = self.get_component_totals("deductions")
self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
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))
self.rounded_total = rounded(self.net_pay) self.rounded_total = rounded(self.net_pay)
self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay')) self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay'))
@ -452,8 +458,6 @@ class SalarySlip(TransactionBase):
else: else:
self.add_tax_components(payroll_period) self.add_tax_components(payroll_period)
self.set_component_amounts_based_on_payment_days(component_type)
def add_structure_components(self, component_type): def add_structure_components(self, component_type):
data = self.get_data_for_eval() data = self.get_data_for_eval()
for struct_row in self._salary_structure_doc.get(component_type): for struct_row in self._salary_structure_doc.get(component_type):
@ -810,7 +814,7 @@ class SalarySlip(TransactionBase):
cint(row.depends_on_payment_days) and cint(self.total_working_days) and cint(row.depends_on_payment_days) and cint(self.total_working_days) and
(not self.salary_slip_based_on_timesheet or (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or getdate(self.start_date) < joining_date or
getdate(self.end_date) > relieving_date (relieving_date and getdate(self.end_date) > relieving_date)
)): )):
additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days) additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("additional_amount")) / cint(self.total_working_days)), row.precision("additional_amount"))
@ -943,15 +947,21 @@ class SalarySlip(TransactionBase):
struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
return struct_row return struct_row
def get_component_totals(self, component_type): def get_component_totals(self, component_type, depends_on_payment_days=0):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
total = 0.0 total = 0.0
for d in self.get(component_type): for d in self.get(component_type):
if not d.do_not_include_in_total: if not d.do_not_include_in_total:
d.amount = flt(d.amount, d.precision("amount")) if depends_on_payment_days:
total += d.amount amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
else:
amount = flt(d.amount, d.precision("amount"))
total += amount
return total return total
def set_component_amounts_based_on_payment_days(self, component_type): def set_component_amounts_based_on_payment_days(self):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
@ -961,8 +971,9 @@ class SalarySlip(TransactionBase):
if not joining_date: if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name))) frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
for d in self.get(component_type): for component_type in ("earnings", "deductions"):
d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0] for d in self.get(component_type):
d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
def set_loan_repayment(self): def set_loan_repayment(self):
self.total_loan_repayment = 0 self.total_loan_repayment = 0
@ -1086,17 +1097,17 @@ class SalarySlip(TransactionBase):
self.calculate_net_pay() self.calculate_net_pay()
def set_totals(self): def set_totals(self):
self.gross_pay = 0 self.gross_pay = 0.0
if self.salary_slip_based_on_timesheet == 1: if self.salary_slip_based_on_timesheet == 1:
self.calculate_total_for_salary_slip_based_on_timesheet() self.calculate_total_for_salary_slip_based_on_timesheet()
else: else:
self.total_deduction = 0 self.total_deduction = 0.0
if self.earnings: if self.earnings:
for earning in self.earnings: for earning in self.earnings:
self.gross_pay += flt(earning.amount) self.gross_pay += flt(earning.amount, earning.precision("amount"))
if self.deductions: if self.deductions:
for deduction in self.deductions: for deduction in self.deductions:
self.total_deduction += flt(deduction.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)
self.set_base_totals() self.set_base_totals()
@ -1125,6 +1136,50 @@ class SalarySlip(TransactionBase):
self.gross_pay += self.earnings[i].amount self.gross_pay += self.earnings[i].amount
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
def compute_year_to_date(self):
year_to_date = 0
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
if payroll_period:
period_start_date = payroll_period.start_date
period_end_date = payroll_period.end_date
else:
# get dates based on fiscal year if no payroll period exists
fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
period_start_date = fiscal_year.year_start_date
period_end_date = fiscal_year.year_end_date
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', period_start_date],
'end_date' : ['<', period_end_date],
'name': ['!=', self.name],
'docstatus': 1
})
year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
year_to_date += self.net_pay
self.year_to_date = year_to_date
def compute_month_to_date(self):
month_to_date = 0
first_day_of_the_month = get_first_day(self.start_date)
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', first_day_of_the_month],
'end_date' : ['<', self.start_date],
'name': ['!=', self.name],
'docstatus': 1
})
month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
month_to_date += self.net_pay
self.month_to_date = month_to_date
def unlink_ref_doc_from_salary_slip(ref_no): def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
where journal_entry=%s and docstatus < 2""", (ref_no)) where journal_entry=%s and docstatus < 2""", (ref_no))

View File

@ -9,7 +9,7 @@ import calendar
import random import random
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
@ -240,7 +240,11 @@ class TestSalarySlip(unittest.TestCase):
interest_income_account='Interest Income Account - _TC', interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC') penalty_income_account='Penalty Income Account - _TC')
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period)
frappe.db.sql("""delete from `tabLoan""") frappe.db.sql("""delete from `tabLoan""")
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1 loan.repay_from_salary = 1
@ -290,6 +294,33 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(salary_slip.gross_pay, 78000) self.assertEqual(salary_slip.gross_pay, 78000)
self.assertEqual(salary_slip.base_gross_pay, 78000*70) self.assertEqual(salary_slip.base_gross_pay, 78000*70)
def test_year_to_date_computation(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
applicant = make_employee("test_ytd@salary.com", company="_Test Company")
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"),
company="_Test Company")
salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD",
"Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
# clear salary slip for this employee
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
create_salary_slips_for_payroll_period(applicant, salary_structure.name,
payroll_period, deduct_random=False)
salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name':
'test_ytd@salary.com'}, order_by = 'posting_date')
year_to_date = 0
for slip in salary_slips:
year_to_date += flt(slip.net_pay)
self.assertEqual(slip.year_to_date, year_to_date)
def test_tax_for_payroll_period(self): def test_tax_for_payroll_period(self):
data = {} data = {}
# test the impact of tax exemption declaration, tax exemption proof submission # test the impact of tax exemption declaration, tax exemption proof submission
@ -410,10 +441,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
employee = frappe.db.get_value("Employee", {"user_id": user}) employee = frappe.db.get_value("Employee", {"user_id": user})
if not frappe.db.exists('Salary Structure', salary_structure): salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee)
else:
salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
if not salary_slip_name: if not salary_slip_name:
@ -557,14 +585,6 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
"amount": 200, "amount": 200,
"exempted_from_income_tax": 1 "exempted_from_income_tax": 1
},
{
"salary_component": 'TDS',
"abbr":'T',
"type": "Deduction",
"depends_on_payment_days": 0,
"variable_based_on_taxable_salary": 1,
"round_to_the_nearest_integer": 1
} }
] ]
if not test_tax: if not test_tax:
@ -575,6 +595,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
"type": "Deduction", "type": "Deduction",
"round_to_the_nearest_integer": 1 "round_to_the_nearest_integer": 1
}) })
else:
data.append({
"salary_component": 'TDS',
"abbr":'T',
"type": "Deduction",
"depends_on_payment_days": 0,
"variable_based_on_taxable_salary": 1,
"round_to_the_nearest_integer": 1
})
if setup or test_tax: if setup or test_tax:
make_salary_component(data, test_tax, company_list) make_salary_component(data, test_tax, company_list)
@ -631,8 +660,13 @@ def create_benefit_claim(employee, payroll_period, amount, component):
}).submit() }).submit()
return claim_date return claim_date
def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None,
frappe.db.sql("""delete from `tabIncome Tax Slab`""") company=None):
if not currency:
currency = erpnext.get_default_currency()
if company:
currency = erpnext.get_company_currency(company)
slabs = [ slabs = [
{ {
@ -652,26 +686,33 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption =
} }
] ]
income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency})
income_tax_slab.name = "Tax Slab: " + payroll_period.name if not income_tax_slab_name:
income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) income_tax_slab = frappe.new_doc("Income Tax Slab")
income_tax_slab.currency = currency income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency)
income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2)
income_tax_slab.company = company or ''
income_tax_slab.currency = currency
if allow_tax_exemption: if allow_tax_exemption:
income_tax_slab.allow_tax_exemption = 1 income_tax_slab.allow_tax_exemption = 1
income_tax_slab.standard_tax_exemption_amount = 50000 income_tax_slab.standard_tax_exemption_amount = 50000
for item in slabs: for item in slabs:
income_tax_slab.append("slabs", item) income_tax_slab.append("slabs", item)
income_tax_slab.append("other_taxes_and_charges", { income_tax_slab.append("other_taxes_and_charges", {
"description": "cess", "description": "cess",
"percent": 4 "percent": 4
}) })
income_tax_slab.save() income_tax_slab.save()
if not dont_submit: if not dont_submit:
income_tax_slab.submit() income_tax_slab.submit()
return income_tax_slab.name
else:
return income_tax_slab_name
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True):
deducted_dates = [] deducted_dates = []

View File

@ -114,7 +114,7 @@ class TestSalaryStructure(unittest.TestCase):
self.assertEqual(sal_struct.currency, 'USD') self.assertEqual(sal_struct.currency, 'USD')
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
test_tax=False, company=None, currency=erpnext.get_default_currency()): test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None):
if test_tax: if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
@ -141,16 +141,24 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
if employee and not frappe.db.get_value("Salary Structure Assignment", if employee and not frappe.db.get_value("Salary Structure Assignment",
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency,
payroll_period=payroll_period)
return salary_structure_doc return salary_structure_doc
def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(),
payroll_period=None):
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
payroll_period = create_payroll_period() if not payroll_period:
create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) payroll_period = create_payroll_period()
income_tax_slab = frappe.db.get_value("Income Tax Slab", {"currency": currency})
if not income_tax_slab:
income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency)
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
salary_structure_assignment.employee = employee salary_structure_assignment.employee = employee
@ -162,7 +170,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.payroll_payable_account = get_payable_account(company)
salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.company = company or erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True) salary_structure_assignment.save(ignore_permissions=True)
salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.income_tax_slab = income_tax_slab
salary_structure_assignment.submit() salary_structure_assignment.submit()
return salary_structure_assignment return salary_structure_assignment

View File

@ -43,7 +43,7 @@ class SalaryStructureAssignment(Document):
def set_payroll_payable_account(self): def set_payroll_payable_account(self):
if not self.payroll_payable_account: if not self.payroll_payable_account:
payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account') payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account')
if not payroll_payable_account: if not payroll_payable_account:
payroll_payable_account = frappe.db.get_value( payroll_payable_account = frappe.db.get_value(
"Account", { "Account", {

View File

@ -543,6 +543,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
company: me.frm.doc.company, company: me.frm.doc.company,
order_type: me.frm.doc.order_type, order_type: me.frm.doc.order_type,
is_pos: cint(me.frm.doc.is_pos), is_pos: cint(me.frm.doc.is_pos),
is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted, is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date, transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule, ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,

View File

@ -0,0 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('E Invoice Request Log', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,103 @@
{
"actions": [],
"autoname": "EINV-REQ-.#####",
"creation": "2020-12-08 12:54:08.175992",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"user",
"url",
"headers",
"response",
"column_break_7",
"timestamp",
"reference_invoice",
"data"
],
"fields": [
{
"fieldname": "user",
"fieldtype": "Link",
"label": "User",
"options": "User"
},
{
"fieldname": "reference_invoice",
"fieldtype": "Link",
"label": "Reference Invoice",
"options": "Sales Invoice"
},
{
"fieldname": "headers",
"fieldtype": "Code",
"label": "Headers",
"options": "JSON"
},
{
"fieldname": "data",
"fieldtype": "Code",
"label": "Data",
"options": "JSON"
},
{
"default": "Now",
"fieldname": "timestamp",
"fieldtype": "Datetime",
"label": "Timestamp"
},
{
"fieldname": "response",
"fieldtype": "Code",
"label": "Response",
"options": "JSON"
},
{
"fieldname": "url",
"fieldtype": "Data",
"label": "URL"
},
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-24 21:09:38.882866",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Request Log",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, 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 EInvoiceRequestLog(Document):
pass

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestEInvoiceRequestLog(unittest.TestCase):
pass

View File

@ -0,0 +1,11 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('E Invoice Settings', {
refresh(frm) {
const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing';
frm.dashboard.set_headline(
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
);
}
});

View File

@ -0,0 +1,58 @@
{
"actions": [],
"creation": "2020-09-24 16:23:16.235722",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable",
"section_break_2",
"credentials",
"auth_token",
"token_expiry"
],
"fields": [
{
"default": "0",
"fieldname": "enable",
"fieldtype": "Check",
"label": "Enable"
},
{
"depends_on": "enable",
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
{
"fieldname": "auth_token",
"fieldtype": "Data",
"hidden": 1,
"read_only": 1
},
{
"fieldname": "token_expiry",
"fieldtype": "Datetime",
"hidden": 1,
"read_only": 1
},
{
"fieldname": "credentials",
"fieldtype": "Table",
"label": "Credentials",
"mandatory_depends_on": "enable",
"options": "E Invoice User"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-12-22 15:34:57.280044",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice Settings",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
class EInvoiceSettings(Document):
def validate(self):
if self.enable and not self.credentials:
frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.'))

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestEInvoiceSettings(unittest.TestCase):
pass

View File

@ -0,0 +1,48 @@
{
"actions": [],
"creation": "2020-12-22 15:02:46.229474",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"gstin",
"username",
"password"
],
"fields": [
{
"fieldname": "gstin",
"fieldtype": "Data",
"in_list_view": 1,
"label": "GSTIN",
"reqd": 1
},
{
"fieldname": "username",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Username",
"reqd": 1
},
{
"fieldname": "password",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Password",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-12-22 15:10:53.466205",
"modified_by": "Administrator",
"module": "Regional",
"name": "E Invoice User",
"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) 2020, 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 EInvoiceUser(Document):
pass

View File

@ -29,25 +29,12 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-09-30 20:08:18.764798", "modified": "2020-12-25 20:20:22.342426",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "UAE VAT Settings", "name": "UAE VAT Settings",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [],
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",

View File

@ -48,9 +48,6 @@ def validate_regional(doc):
def missing(field_label, regulation): def missing(field_label, regulation):
"""Notify the user that a required field is missing.""" """Notify the user that a required field is missing."""
context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.' translated_msg = _('Remember to set {field_label}. It is required by {regulation}.', context='Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.') # noqa: E501
msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format( formatted_msg = translated_msg.format(field_label=frappe.bold(_(field_label)), regulation=regulation)
field_label=frappe.bold(_(field_label)), msgprint(formatted_msg)
regulation=regulation
)
)

View File

@ -0,0 +1,12 @@
import frappe
import unittest
from erpnext.regional.germany.accounts_controller import validate_regional
class TestAccountsController(unittest.TestCase):
def setUp(self):
self.sales_invoice = frappe.get_last_doc('Sales Invoice')
def test_validate_regional(self):
validate_regional(self.sales_invoice)

View File

@ -0,0 +1,31 @@
{{
"SlNo": "{item.sr_no}",
"PrdDesc": "{item.description}",
"IsServc": "{item.is_service_item}",
"HsnCd": "{item.gst_hsn_code}",
"Barcde": "{item.barcode}",
"Unit": "{item.uom}",
"Qty": "{item.qty}",
"FreeQty": "{item.free_qty}",
"UnitPrice": "{item.unit_rate}",
"TotAmt": "{item.gross_amount}",
"Discount": "{item.discount_amount}",
"AssAmt": "{item.taxable_value}",
"PrdSlNo": "{item.serial_no}",
"GstRt": "{item.tax_rate}",
"IgstAmt": "{item.igst_amount}",
"CgstAmt": "{item.cgst_amount}",
"SgstAmt": "{item.sgst_amount}",
"CesRt": "{item.cess_rate}",
"CesAmt": "{item.cess_amount}",
"CesNonAdvlAmt": "{item.cess_nadv_amount}",
"StateCesRt": "{item.state_cess_rate}",
"StateCesAmt": "{item.state_cess_amount}",
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
"OthChrg": "{item.other_charges}",
"TotItemVal": "{item.total_value}",
"BchDtls": {{
"Nm": "{item.batch_no}",
"ExpDt": "{item.batch_expiry_date}"
}}
}}

View File

@ -0,0 +1,110 @@
{{
"Version": "1.1",
"TranDtls": {{
"TaxSch": "{transaction_details.tax_scheme}",
"SupTyp": "{transaction_details.supply_type}",
"RegRev": "{transaction_details.reverse_charge}",
"EcmGstin": "{transaction_details.ecom_gstin}",
"IgstOnIntra": "{transaction_details.igst_on_intra}"
}},
"DocDtls": {{
"Typ": "{doc_details.invoice_type}",
"No": "{doc_details.invoice_name}",
"Dt": "{doc_details.invoice_date}"
}},
"SellerDtls": {{
"Gstin": "{seller_details.gstin}",
"LglNm": "{seller_details.legal_name}",
"TrdNm": "{seller_details.trade_name}",
"Loc": "{seller_details.location}",
"Pin": "{seller_details.pincode}",
"Stcd": "{seller_details.state_code}",
"Addr1": "{seller_details.address_line1}",
"Addr2": "{seller_details.address_line2}",
"Ph": "{seller_details.phone}",
"Em": "{seller_details.email}"
}},
"BuyerDtls": {{
"Gstin": "{buyer_details.gstin}",
"LglNm": "{buyer_details.legal_name}",
"TrdNm": "{buyer_details.trade_name}",
"Addr1": "{buyer_details.address_line1}",
"Addr2": "{buyer_details.address_line2}",
"Loc": "{buyer_details.location}",
"Pin": "{buyer_details.pincode}",
"Stcd": "{buyer_details.state_code}",
"Ph": "{buyer_details.phone}",
"Em": "{buyer_details.email}",
"Pos": "{buyer_details.place_of_supply}"
}},
"DispDtls": {{
"Nm": "{dispatch_details.company_name}",
"Addr1": "{dispatch_details.address_line1}",
"Addr2": "{dispatch_details.address_line2}",
"Loc": "{dispatch_details.location}",
"Pin": "{dispatch_details.pincode}",
"Stcd": "{dispatch_details.state_code}"
}},
"ShipDtls": {{
"Gstin": "{shipping_details.gstin}",
"LglNm": "{shipping_details.legal_name}",
"TrdNm": "{shipping_details.trader_name}",
"Addr1": "{shipping_details.address_line1}",
"Addr2": "{shipping_details.address_line2}",
"Loc": "{shipping_details.location}",
"Pin": "{shipping_details.pincode}",
"Stcd": "{shipping_details.state_code}"
}},
"ItemList": [
{item_list}
],
"ValDtls": {{
"AssVal": "{invoice_value_details.base_total}",
"CgstVal": "{invoice_value_details.total_cgst_amt}",
"SgstVal": "{invoice_value_details.total_sgst_amt}",
"IgstVal": "{invoice_value_details.total_igst_amt}",
"CesVal": "{invoice_value_details.total_cess_amt}",
"Discount": "{invoice_value_details.invoice_discount_amt}",
"RndOffAmt": "{invoice_value_details.round_off}",
"OthChrg": "{invoice_value_details.total_other_charges}",
"TotInvVal": "{invoice_value_details.base_grand_total}",
"TotInvValFc": "{invoice_value_details.grand_total}"
}},
"PayDtls": {{
"Nm": "{payment_details.payee_name}",
"AccDet": "{payment_details.account_no}",
"Mode": "{payment_details.mode_of_payment}",
"FinInsBr": "{payment_details.ifsc_code}",
"PayTerm": "{payment_details.terms}",
"PaidAmt": "{payment_details.paid_amount}",
"PaymtDue": "{payment_details.outstanding_amount}"
}},
"RefDtls": {{
"DocPerdDtls": {{
"InvStDt": "{period_details.start_date}",
"InvEndDt": "{period_details.end_date}"
}},
"PrecDocDtls": [{{
"InvNo": "{prev_doc_details.invoice_name}",
"InvDt": "{prev_doc_details.invoice_date}"
}}]
}},
"ExpDtls": {{
"ShipBNo": "{export_details.bill_no}",
"ShipBDt": "{export_details.bill_date}",
"Port": "{export_details.port}",
"ForCur": "{export_details.foreign_curr_code}",
"CntCode": "{export_details.country_code}",
"ExpDuty": "{export_details.export_duty}"
}},
"EwbDtls": {{
"TransId": "{eway_bill_details.gstin}",
"TransName": "{eway_bill_details.name}",
"TransMode": "{eway_bill_details.mode_of_transport}",
"Distance": "{eway_bill_details.distance}",
"TransDocNo": "{eway_bill_details.document_name}",
"TransDocDt": "{eway_bill_details.document_date}",
"VehNo": "{eway_bill_details.vehicle_no}",
"VehType": "{eway_bill_details.vehicle_type}"
}}
}}

View File

@ -0,0 +1,956 @@
{
"Version": {
"type": "string",
"minLength": 1,
"maxLength": 6,
"description": "Version of the schema"
},
"Irn": {
"type": "string",
"minLength": 64,
"maxLength": 64,
"description": "Invoice Reference Number"
},
"TranDtls": {
"type": "object",
"properties": {
"TaxSch": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["GST"],
"description": "GST- Goods and Services Tax Scheme"
},
"SupTyp": {
"type": "string",
"minLength": 3,
"maxLength": 10,
"enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"],
"description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export"
},
"RegRev": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"],
"description": "Y- whether the tax liability is payable under reverse charge"
},
"EcmGstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})",
"description": "E-Commerce GSTIN",
"validationMsg": "E-Commerce GSTIN is invalid"
},
"IgstOnIntra": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"],
"description": "Y- indicates the supply is intra state but chargeable to IGST"
}
},
"required": ["TaxSch", "SupTyp"]
},
"DocDtls": {
"type": "object",
"properties": {
"Typ": {
"type": "string",
"minLength": 3,
"maxLength": 3,
"enum": ["INV", "CRN", "DBN"],
"description": "Document Type"
},
"No": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
"description": "Document Number",
"validationMsg": "Document Number should not be starting with 0, / and -"
},
"Dt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Document Date"
}
},
"required": ["Typ", "No", "Dt"]
},
"SellerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"pattern": "([0-9]{2}[0-9A-Z]{13})",
"description": "Supplier GSTIN",
"validationMsg": "Company GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Legal Name"
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Tradename"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 50,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "Supplier State Code"
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12,
"description": "Phone"
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100,
"description": "Email-Id"
}
},
"required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"BuyerDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"minLength": 3,
"maxLength": 15,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"description": "Buyer GSTIN",
"validationMsg": "Customer GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Legal Name"
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Trade Name"
},
"Pos": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "Place of Supply State code"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "Buyer State Code"
},
"Ph": {
"type": "string",
"minLength": 6,
"maxLength": 12,
"description": "Phone"
},
"Em": {
"type": "string",
"minLength": 6,
"maxLength": 100,
"description": "Email-Id"
}
},
"required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
},
"DispDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Dispatch Address Name"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "State Code"
}
},
"required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ShipDtls": {
"type": "object",
"properties": {
"Gstin": {
"type": "string",
"maxLength": 15,
"minLength": 3,
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
"description": "Shipping Address GSTIN",
"validationMsg": "Shipping Address GSTIN is invalid"
},
"LglNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Legal Name"
},
"TrdNm": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Trade Name"
},
"Addr1": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Address Line 1"
},
"Addr2": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Address Line 2"
},
"Loc": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Location"
},
"Pin": {
"type": "number",
"minimum": 100000,
"maximum": 999999,
"description": "Pincode"
},
"Stcd": {
"type": "string",
"minLength": 1,
"maxLength": 2,
"description": "State Code"
}
},
"required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
},
"ItemList": {
"type": "Array",
"properties": {
"SlNo": {
"type": "string",
"minLength": 1,
"maxLength": 6,
"description": "Serial No. of Item"
},
"PrdDesc": {
"type": "string",
"minLength": 3,
"maxLength": 300,
"description": "Item Name"
},
"IsServc": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["Y", "N"],
"description": "Is Service Item"
},
"HsnCd": {
"type": "string",
"minLength": 4,
"maxLength": 8,
"description": "HSN Code"
},
"Barcde": {
"type": "string",
"minLength": 3,
"maxLength": 30,
"description": "Barcode"
},
"Qty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999,
"description": "Quantity"
},
"FreeQty": {
"type": "number",
"minimum": 0,
"maximum": 9999999999.999,
"description": "Free Quantity"
},
"Unit": {
"type": "string",
"minLength": 3,
"maxLength": 8,
"description": "UOM"
},
"UnitPrice": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.999,
"description": "Rate"
},
"TotAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Gross Amount"
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Discount"
},
"PreTaxVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Pre tax value"
},
"AssAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Taxable Value"
},
"GstRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999,
"description": "GST Rate"
},
"IgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "IGST Amount"
},
"CgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "CGST Amount"
},
"SgstAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "SGST Amount"
},
"CesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999,
"description": "Cess Rate"
},
"CesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Cess Amount (Advalorem)"
},
"CesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Cess Amount (Non-Advalorem)"
},
"StateCesRt": {
"type": "number",
"minimum": 0,
"maximum": 999.999,
"description": "State CESS Rate"
},
"StateCesAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "State CESS Amount"
},
"StateCesNonAdvlAmt": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "State CESS Amount (Non Advalorem)"
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Other Charges"
},
"TotItemVal": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Total Item Value"
},
"OrdLineRef": {
"type": "string",
"minLength": 1,
"maxLength": 50,
"description": "Order line reference"
},
"OrgCntry": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "Origin Country"
},
"PrdSlNo": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"description": "Serial number"
},
"BchDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 3,
"maxLength": 20,
"description": "Batch number"
},
"ExpDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Batch Expiry Date"
},
"WrDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Warranty Date"
}
},
"required": ["Nm"]
},
"AttribDtls": {
"type": "Array",
"Attribute": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Attribute name of the item"
},
"Val": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Attribute value of the item"
}
}
}
}
},
"required": [
"SlNo",
"IsServc",
"HsnCd",
"UnitPrice",
"TotAmt",
"AssAmt",
"GstRt",
"TotItemVal"
]
},
"ValDtls": {
"type": "object",
"properties": {
"AssVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total Assessable value of all items"
},
"CgstVal": {
"type": "number",
"maximum": 99999999999999.99,
"minimum": 0,
"description": "Total CGST value of all items"
},
"SgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total SGST value of all items"
},
"IgstVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total IGST value of all items"
},
"CesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total CESS value of all items"
},
"StCesVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Total State CESS value of all items"
},
"Discount": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Invoice Discount"
},
"OthChrg": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Other Charges"
},
"RndOffAmt": {
"type": "number",
"minimum": -99.99,
"maximum": 99.99,
"description": "Rounded off Amount"
},
"TotInvVal": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Final Invoice Value "
},
"TotInvValFc": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Final Invoice value in Foreign Currency"
}
},
"required": ["AssVal", "TotInvVal"]
},
"PayDtls": {
"type": "object",
"properties": {
"Nm": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Payee Name"
},
"AccDet": {
"type": "string",
"minLength": 1,
"maxLength": 18,
"description": "Bank Account Number of Payee"
},
"Mode": {
"type": "string",
"minLength": 1,
"maxLength": 18,
"description": "Mode of Payment"
},
"FinInsBr": {
"type": "string",
"minLength": 1,
"maxLength": 11,
"description": "Branch or IFSC code"
},
"PayTerm": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Terms of Payment"
},
"PayInstr": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Payment Instruction"
},
"CrTrn": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Credit Transfer"
},
"DirDr": {
"type": "string",
"minLength": 1,
"maxLength": 100,
"description": "Direct Debit"
},
"CrDay": {
"type": "number",
"minimum": 0,
"maximum": 9999,
"description": "Credit Days"
},
"PaidAmt": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Advance Amount"
},
"PaymtDue": {
"type": "number",
"minimum": 0,
"maximum": 99999999999999.99,
"description": "Outstanding Amount"
}
}
},
"RefDtls": {
"type": "object",
"properties": {
"InvRm": {
"type": "string",
"maxLength": 100,
"minLength": 3,
"pattern": "^[0-9A-Za-z/-]{3,100}$",
"description": "Remarks/Note"
},
"DocPerdDtls": {
"type": "object",
"properties": {
"InvStDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Invoice Period Start Date"
},
"InvEndDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Invoice Period End Date"
}
},
"required": ["InvStDt ", "InvEndDt "]
},
"PrecDocDtls": {
"type": "object",
"properties": {
"InvNo": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$",
"description": "Reference of Original Invoice"
},
"InvDt": {
"type": "string",
"maxLength": 10,
"minLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Date of Orginal Invoice"
},
"OthRefNo": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"description": "Other Reference"
}
}
},
"required": ["InvNo", "InvDt"],
"ContrDtls": {
"type": "object",
"properties": {
"RecAdvRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Receipt Advice No."
},
"RecAdvDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Date of receipt advice"
},
"TendRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Lot/Batch Reference No."
},
"ContrRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Contract Reference Number"
},
"ExtRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Any other reference"
},
"ProjRefr": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"pattern": "^([0-9A-Za-z/-]){1,20}$",
"description": "Project Reference Number"
},
"PORefr": {
"type": "string",
"minLength": 1,
"maxLength": 16,
"pattern": "^([0-9A-Za-z/-]){1,16}$",
"description": "PO Reference Number"
},
"PORefDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "PO Reference date"
}
}
}
}
},
"AddlDocDtls": {
"type": "Array",
"properties": {
"Url": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Supporting document URL"
},
"Docs": {
"type": "string",
"minLength": 3,
"maxLength": 1000,
"description": "Supporting document in Base64 Format"
},
"Info": {
"type": "string",
"minLength": 3,
"maxLength": 1000,
"description": "Any additional information"
}
}
},
"ExpDtls": {
"type": "object",
"properties": {
"ShipBNo": {
"type": "string",
"minLength": 1,
"maxLength": 20,
"description": "Shipping Bill No."
},
"ShipBDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Shipping Bill Date"
},
"Port": {
"type": "string",
"minLength": 2,
"maxLength": 10,
"pattern": "^[0-9A-Za-z]{2,10}$",
"description": "Port Code. Refer the master"
},
"RefClm": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"description": "Claiming Refund. Y/N"
},
"ForCur": {
"type": "string",
"minLength": 3,
"maxLength": 16,
"description": "Additional Currency Code. Refer the master"
},
"CntCode": {
"type": "string",
"minLength": 2,
"maxLength": 2,
"description": "Country Code. Refer the master"
},
"ExpDuty": {
"type": "number",
"minimum": 0,
"maximum": 999999999999.99,
"description": "Export Duty"
}
}
},
"EwbDtls": {
"type": "object",
"properties": {
"TransId": {
"type": "string",
"minLength": 15,
"maxLength": 15,
"description": "Transporter GSTIN"
},
"TransName": {
"type": "string",
"minLength": 3,
"maxLength": 100,
"description": "Transporter Name"
},
"TransMode": {
"type": "string",
"maxLength": 1,
"minLength": 1,
"enum": ["1", "2", "3", "4"],
"description": "Mode of Transport"
},
"Distance": {
"type": "number",
"minimum": 1,
"maximum": 9999,
"description": "Distance"
},
"TransDocNo": {
"type": "string",
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$",
"description": "Tranport Document Number"
},
"TransDocDt": {
"type": "string",
"minLength": 10,
"maxLength": 10,
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
"description": "Transport Document Date"
},
"VehNo": {
"type": "string",
"minLength": 4,
"maxLength": 20,
"description": "Vehicle Number"
},
"VehType": {
"type": "string",
"minLength": 1,
"maxLength": 1,
"enum": ["O", "R"],
"description": "Vehicle Type"
}
},
"required": ["Distance"]
},
"required": [
"Version",
"TranDtls",
"DocDtls",
"SellerDtls",
"BuyerDtls",
"ItemList",
"ValDtls"
]
}

View File

@ -0,0 +1,305 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
refresh(frm) {
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
const add_custom_button = (label, action) => {
if (!frm.custom_buttons[label]) {
frm.add_custom_button(label, action, __('E Invoicing'));
}
};
if (!irn && !__unsaved) {
const action = () => {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
args: { doctype, docname: name },
freeze: true,
callback: (res) => {
const einvoice = res.message;
show_einvoice_preview(frm, einvoice);
}
});
};
add_custom_button(__("Generate IRN"), action);
}
if (irn && !irn_cancelled && !ewaybill) {
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const action = () => {
const d = new frappe.ui.Dialog({
title: __("Cancel IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
args: {
doctype,
docname: name,
irn: irn,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Cancel IRN"), action);
}
if (irn && !irn_cancelled && !ewaybill) {
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Generate E-Way Bill'),
wide: 1,
fields: get_ewaybill_fields(frm),
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill',
args: {
doctype,
docname: name,
irn,
...data
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Generate E-Way Bill"), action);
}
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Cancel E-Way Bill'),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: {
doctype,
docname: name,
eway_bill: ewaybill,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
callback: () => frm.reload_doc() || d.hide(),
error: () => d.hide()
});
},
primary_action_label: __('Submit')
});
d.show();
};
add_custom_button(__("Cancel E-Way Bill"), action);
}
}
});
};
const get_ewaybill_fields = (frm) => {
return [
{
'fieldname': 'transporter',
'label': 'Transporter',
'fieldtype': 'Link',
'options': 'Supplier',
'default': frm.doc.transporter
},
{
'fieldname': 'gst_transporter_id',
'label': 'GST Transporter ID',
'fieldtype': 'Data',
'fetch_from': 'transporter.gst_transporter_id',
'default': frm.doc.gst_transporter_id
},
{
'fieldname': 'driver',
'label': 'Driver',
'fieldtype': 'Link',
'options': 'Driver',
'default': frm.doc.driver
},
{
'fieldname': 'lr_no',
'label': 'Transport Receipt No',
'fieldtype': 'Data',
'default': frm.doc.lr_no
},
{
'fieldname': 'vehicle_no',
'label': 'Vehicle No',
'fieldtype': 'Data',
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.vehicle_no
},
{
'fieldname': 'distance',
'label': 'Distance (in km)',
'fieldtype': 'Float',
'default': frm.doc.distance
},
{
'fieldname': 'transporter_col_break',
'fieldtype': 'Column Break',
},
{
'fieldname': 'transporter_name',
'label': 'Transporter Name',
'fieldtype': 'Data',
'fetch_from': 'transporter.name',
'read_only': 1,
'default': frm.doc.transporter_name
},
{
'fieldname': 'mode_of_transport',
'label': 'Mode of Transport',
'fieldtype': 'Select',
'options': `\nRoad\nAir\nRail\nShip`,
'default': frm.doc.mode_of_transport
},
{
'fieldname': 'driver_name',
'label': 'Driver Name',
'fieldtype': 'Data',
'fetch_from': 'driver.full_name',
'read_only': 1,
'default': frm.doc.driver_name
},
{
'fieldname': 'lr_date',
'label': 'Transport Receipt Date',
'fieldtype': 'Date',
'default': frm.doc.lr_date
},
{
'fieldname': 'gst_vehicle_type',
'label': 'GST Vehicle Type',
'fieldtype': 'Select',
'options': `Regular\nOver Dimensional Cargo (ODC)`,
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.gst_vehicle_type
}
];
};
const request_irn_generation = (frm) => {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_irn',
args: { doctype: frm.doc.doctype, docname: frm.doc.name },
freeze: true,
callback: () => frm.reload_doc()
});
};
const get_preview_dialog = (frm, action) => {
const dialog = new frappe.ui.Dialog({
title: __("Preview"),
wide: 1,
fields: [
{
"label": "Preview",
"fieldname": "preview_html",
"fieldtype": "HTML"
}
],
primary_action: () => action(frm) || dialog.hide(),
primary_action_label: __('Generate IRN')
});
return dialog;
};
const show_einvoice_preview = (frm, einvoice) => {
const preview_dialog = get_preview_dialog(frm, request_irn_generation);
// initialize e-invoice fields
einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate();
frm.doc.signed_einvoice = JSON.stringify(einvoice);
// initialize preview wrapper
const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper;
$preview_wrapper.html(
`<div>
<div class="print-preview">
<div class="print-format"></div>
</div>
<div class="page-break-message text-muted text-center text-medium margin-top"></div>
</div>`
);
frappe.call({
method: "frappe.www.printview.get_html_and_style",
args: {
doc: frm.doc,
print_format: "GST E-Invoice",
no_letterhead: 1
},
callback: function (r) {
if (!r.exc) {
$preview_wrapper.find(".print-format").html(r.message.html);
const style = `
.print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; }
.print-preview { min-height: 0px; }
.modal-dialog { width: 720px; }`;
frappe.dom.set_style(style, "custom-print-style");
preview_dialog.show();
}
}
});
};

View File

@ -0,0 +1,803 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import os
import re
import jwt
import sys
import json
import base64
import frappe
import traceback
from frappe import _, bold
from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
def validate_einvoice_fields(doc):
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
invalid_doctype = doc.doctype not in ['Sales Invoice']
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return
if doc.docstatus == 0 and doc._action == 'save':
if doc.irn:
frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed'))
if len(doc.name) > 16:
raise_document_name_too_long_error()
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
def raise_document_name_too_long_error():
title = _('Document ID Too Long')
msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice, ')
msg += _('document id {} exceed 16 letters. ').format(bold(_('should not')))
msg += '<br><br>'
msg += _('You must {} your {} in order to have document id of {} length 16. ').format(
bold(_('modify')), bold(_('naming series')), bold(_('maximum'))
)
msg += _('Please account for ammended documents too. ')
frappe.throw(msg, title=title)
def read_json(name):
file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name))
with open(file_path, 'r') as f:
return cstr(f.read())
def get_transaction_details(invoice):
supply_type = ''
if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
if not supply_type:
rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export')
frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export),
title=_('Invalid Supply Type'))
return frappe._dict(dict(
tax_scheme='GST',
supply_type=supply_type,
reverse_charge=invoice.reverse_charge
))
def get_doc_details(invoice):
invoice_type = 'CRN' if invoice.is_return else 'INV'
invoice_name = invoice.name
invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
return frappe._dict(dict(
invoice_type=invoice_type,
invoice_name=invoice_name,
invoice_date=invoice_date
))
def get_party_details(address_name):
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
if (not d.gstin
or not d.city
or not d.pincode
or not d.address_title
or not d.address_line1
or not d.gst_state_number):
frappe.throw(
msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
get_link_to_form('Address', address_name)
),
title=_('Missing Address Fields')
)
if d.gst_state_number == 97:
# according to einvoice standard
pincode = 999999
return frappe._dict(dict(
gstin=d.gstin, legal_name=d.address_title,
location=d.city, pincode=d.pincode,
state_code=d.gst_state_number,
address_line1=d.address_line1,
address_line2=d.address_line2
))
def get_gstin_details(gstin):
if not hasattr(frappe.local, 'gstin_cache'):
frappe.local.gstin_cache = {}
key = gstin
details = frappe.local.gstin_cache.get(key)
if details:
return details
details = frappe.cache().hget('gstin_cache', key)
if details:
frappe.local.gstin_cache[key] = details
return details
if not details:
return GSPConnector.get_gstin_details(gstin)
def get_overseas_address_details(address_name):
address_title, address_line1, address_line2, city = frappe.db.get_value(
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
)
if not address_title or not address_line1 or not city:
frappe.throw(
msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
get_link_to_form('Address', address_name)
),
title=_('Missing Address Fields')
)
return frappe._dict(dict(
gstin='URP', legal_name=address_title, location=city,
address_line1=address_line1, address_line2=address_line2,
pincode=999999, state_code=96, place_of_supply=96
))
def get_item_list(invoice):
item_list = []
for d in invoice.items:
einvoice_item_schema = read_json('einv_item_template')
item = frappe._dict({})
item.update(d.as_dict())
item.sr_no = d.idx
item.description = d.item_name.replace('"', '\\"')
item.qty = abs(item.qty)
item.discount_amount = abs(item.discount_amount * item.qty)
item.unit_rate = abs(item.base_amount / item.qty)
item.gross_amount = abs(item.base_amount)
item.taxable_value = abs(item.base_amount)
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
item.serial_no = ""
item = update_item_taxes(invoice, item)
item.total_value = abs(
item.taxable_value + item.igst_amount + item.sgst_amount +
item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges
)
einv_item = einvoice_item_schema.format(item=item)
item_list.append(einv_item)
return ', '.join(item_list)
def update_item_taxes(invoice, item):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
for attr in [
'tax_rate', 'cess_rate', 'cess_nadv_amount',
'cgst_amount', 'sgst_amount', 'igst_amount',
'cess_amount', 'cess_nadv_amount', 'other_charges'
]:
item[attr] = 0
for t in invoice.taxes:
# this contains item wise tax rate & tax amount (incl. discount)
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
if t.account_head in gst_accounts_list:
item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount
item_tax_amount = (item_tax_rate / 100) * item.base_amount
if t.account_head in gst_accounts.cess_account:
item_tax_amount_after_discount = item_tax_detail[1]
if t.charge_type == 'On Item Quantity':
item.cess_nadv_amount += abs(item_tax_amount_after_discount)
else:
item.cess_rate += item_tax_rate
item.cess_amount += abs(item_tax_amount_after_discount)
for tax_type in ['igst', 'cgst', 'sgst']:
if t.account_head in gst_accounts[f'{tax_type}_account']:
item.tax_rate += item_tax_rate
item[f'{tax_type}_amount'] += abs(item_tax_amount)
return item
def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
invoice_value_details.base_total = abs(invoice.base_total)
invoice_value_details.invoice_discount_amt = invoice.discount_amount
invoice_value_details.round_off = invoice.rounding_adjustment
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
invoice_value_details = update_invoice_taxes(invoice, invoice_value_details)
return invoice_value_details
def update_invoice_taxes(invoice, invoice_value_details):
gst_accounts = get_gst_accounts(invoice.company)
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
invoice_value_details.total_cgst_amt = 0
invoice_value_details.total_sgst_amt = 0
invoice_value_details.total_igst_amt = 0
invoice_value_details.total_cess_amt = 0
invoice_value_details.total_other_charges = 0
for t in invoice.taxes:
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
# using after discount amt since item also uses after discount amt for cess calc
invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
for tax_type in ['igst', 'cgst', 'sgst']:
if t.account_head in gst_accounts[f'{tax_type}_account']:
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount)
else:
invoice_value_details.total_other_charges += abs(t.base_tax_amount)
return invoice_value_details
def get_payment_details(invoice):
payee_name = invoice.company
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
paid_amount = invoice.base_paid_amount
outstanding_amount = invoice.outstanding_amount
return frappe._dict(dict(
payee_name=payee_name, mode_of_payment=mode_of_payment,
paid_amount=paid_amount, outstanding_amount=outstanding_amount
))
def get_return_doc_reference(invoice):
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
return frappe._dict(dict(
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
))
def get_eway_bill_details(invoice):
if invoice.is_return:
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed'))
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
return frappe._dict(dict(
gstin=invoice.gst_transporter_id,
name=invoice.transporter_name,
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
distance=invoice.distance or 0,
document_name=invoice.lr_no,
document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
vehicle_no=invoice.vehicle_no,
vehicle_type=vehicle_type[invoice.gst_vehicle_type]
))
def validate_mandatory_fields(invoice):
if not invoice.company_address:
frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
if not invoice.customer_address:
frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
title=_('Missing Fields')
)
if not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
frappe.throw(
_('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
title=_('Missing Fields')
)
def make_einvoice(invoice):
validate_mandatory_fields(invoice)
schema = read_json('einv_template')
transaction_details = get_transaction_details(invoice)
item_list = get_item_list(invoice)
doc_details = get_doc_details(invoice)
invoice_value_details = get_invoice_value_details(invoice)
seller_details = get_party_details(invoice.company_address)
if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
buyer_details = get_party_details(invoice.customer_address)
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
place_of_supply = place_of_supply[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
shipping_details = get_party_details(invoice.shipping_address_name)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter:
eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented
dispatch_details = period_details = export_details = frappe._dict({})
einvoice = schema.format(
transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details,
seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details,
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
einvoice = json.loads(einvoice)
validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice)
if errors:
message = "\n".join([
"E Invoice: ", json.dumps(einvoice, indent=4),
"-" * 50,
"Errors: ", json.dumps(errors, indent=4)
])
frappe.log_error(title="E Invoice Validation Failed", message=message)
frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1)
return einvoice
def validate_einvoice(validations, einvoice, errors=[]):
for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None)
if not value or value == "None":
# remove keys with empty values
einvoice.pop(fieldname, None)
continue
value_type = field_validation.get("type").lower()
if value_type in ['object', 'array']:
child_validations = field_validation.get('properties')
if isinstance(value, list):
for d in value:
validate_einvoice(child_validations, d, errors)
if not d:
# remove empty dicts
einvoice.pop(fieldname, None)
else:
validate_einvoice(child_validations, value, errors)
if not value:
# remove empty dicts
einvoice.pop(fieldname, None)
continue
# convert to int or str
if value_type == 'string':
einvoice[fieldname] = str(value)
elif value_type == 'number':
is_integer = '.' not in str(field_validation.get('maximum'))
precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
value = einvoice[fieldname]
max_length = field_validation.get('maxLength')
minimum = flt(field_validation.get('minimum'))
maximum = flt(field_validation.get('maximum'))
pattern_str = field_validation.get('pattern')
pattern = re.compile(pattern_str or '')
label = field_validation.get('description') or fieldname
if value_type == 'string' and len(value) > max_length:
errors.append(_('{} should not exceed {} characters').format(label, max_length))
if value_type == 'number' and (value > maximum or value < minimum):
errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
if pattern_str and not pattern.match(value):
errors.append(field_validation.get('validationMsg'))
return errors
class RequestFailed(Exception): pass
class GSPConnector():
def __init__(self, doctype=None, docname=None):
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
self.credentials = self.get_credentials()
self.base_url = 'https://gsp.adaequare.com'
self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token'
self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
def get_credentials(self):
if self.invoice:
gstin = self.get_seller_gstin()
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
else:
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
return credentials
def get_seller_gstin(self):
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
if not gstin:
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
return gstin
def get_auth_token(self):
if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0:
self.fetch_auth_token()
return self.e_invoice_settings.auth_token
def make_request(self, request_type, url, headers=None, data=None):
if request_type == 'post':
res = make_post_request(url, headers=headers, data=data)
else:
res = make_get_request(url, headers=headers, data=data)
self.log_request(url, headers, data, res)
return res
def log_request(self, url, headers, data, res):
headers.update({ 'password': self.credentials.password })
request_log = frappe.get_doc({
"doctype": "E Invoice Request Log",
"user": frappe.session.user,
"reference_invoice": self.invoice.name if self.invoice else None,
"url": url,
"headers": json.dumps(headers, indent=4) if headers else None,
"data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
"response": json.dumps(res, indent=4) if res else None
})
request_log.insert(ignore_permissions=True)
frappe.db.commit()
def fetch_auth_token(self):
headers = {
'gspappid': frappe.conf.einvoice_client_id,
'gspappsecret': frappe.conf.einvoice_client_secret
}
res = {}
try:
res = self.make_request('post', self.authenticate_url, headers)
self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
self.e_invoice_settings.save()
except Exception:
self.log_error(res)
self.raise_error(True)
def get_headers(self):
return {
'content-type': 'application/json',
'user_name': self.credentials.username,
'password': self.credentials.get_password(),
'gstin': self.credentials.gstin,
'authorization': self.get_auth_token(),
'requestid': str(base64.b64encode(os.urandom(18))),
}
def fetch_gstin_details(self, gstin):
headers = self.get_headers()
try:
params = '?gstin={gstin}'.format(gstin=gstin)
res = self.make_request('get', self.gstin_details_url + params, headers)
if res.get('success'):
return res.get('result')
else:
self.log_error(res)
raise RequestFailed
except RequestFailed:
self.raise_error()
except Exception:
self.log_error()
self.raise_error(True)
@staticmethod
def get_gstin_details(gstin):
'''fetch and cache GSTIN details'''
if not hasattr(frappe.local, 'gstin_cache'):
frappe.local.gstin_cache = {}
key = gstin
gsp_connector = GSPConnector()
details = gsp_connector.fetch_gstin_details(gstin)
frappe.local.gstin_cache[key] = details
frappe.cache().hset('gstin_cache', key, details)
return details
def generate_irn(self):
headers = self.get_headers()
einvoice = make_einvoice(self.invoice)
data = json.dumps(einvoice, indent=4)
try:
res = self.make_request('post', self.generate_irn_url, headers, data)
if res.get('success'):
self.set_einvoice_data(res.get('result'))
elif '2150' in res.get('message'):
# IRN already generated but not updated in invoice
# Extract the IRN from the response description and fetch irn details
irn = res.get('result')[0].get('Desc').get('Irn')
irn_details = self.get_irn_details(irn)
if irn_details:
self.set_einvoice_data(irn_details)
else:
raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \
Contact ERPNext support to resolve the issue.')
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def get_irn_details(self, irn):
headers = self.get_headers()
try:
params = '?irn={irn}'.format(irn=irn)
res = self.make_request('get', self.irn_details_url + params, headers)
if res.get('success'):
return res.get('result')
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error()
self.raise_error(True)
def cancel_irn(self, irn, reason, remark):
headers = self.get_headers()
data = json.dumps({
'Irn': irn,
'Cnlrsn': reason,
'Cnlrem': remark
}, indent=4)
try:
res = self.make_request('post', self.cancel_irn_url, headers, data)
if res.get('success'):
self.invoice.irn_cancelled = 1
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('IRN Cancelled - {}').format(remark)
}
self.update_invoice()
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def generate_eway_bill(self, **kwargs):
args = frappe._dict(kwargs)
headers = self.get_headers()
eway_bill_details = get_eway_bill_details(args)
data = json.dumps({
'Irn': args.irn,
'Distance': cint(eway_bill_details.distance),
'TransMode': eway_bill_details.mode_of_transport,
'TransId': eway_bill_details.gstin,
'TransName': eway_bill_details.transporter,
'TrnDocDt': eway_bill_details.document_date,
'TrnDocNo': eway_bill_details.document_name,
'VehNo': eway_bill_details.vehicle_no,
'VehType': eway_bill_details.vehicle_type
}, indent=4)
try:
res = self.make_request('post', self.generate_ewaybill_url, headers, data)
if res.get('success'):
self.invoice.ewaybill = res.get('result').get('EwbNo')
self.invoice.eway_bill_cancelled = 0
self.invoice.update(args)
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('E-Way Bill Generated')
}
self.update_invoice()
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def cancel_eway_bill(self, eway_bill, reason, remark):
headers = self.get_headers()
data = json.dumps({
'ewbNo': eway_bill,
'cancelRsnCode': reason,
'cancelRmrk': remark
}, indent=4)
try:
res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
if res.get('success'):
self.invoice.ewaybill = ''
self.invoice.eway_bill_cancelled = 1
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('E-Way Bill Cancelled - {}').format(remark)
}
self.update_invoice()
else:
raise RequestFailed
except RequestFailed:
errors = self.sanitize_error_message(res.get('message'))
self.raise_error(errors=errors)
except Exception:
self.log_error(data)
self.raise_error(True)
def sanitize_error_message(self, message):
'''
On validation errors, response message looks something like this:
message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
3095 : Supplier GSTIN is inactive'
we search for string between ':' to extract the error messages
errors = [
': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
': Test'
]
then we trim down the message by looping over errors
'''
errors = re.findall(': [^:]+', message)
for idx, e in enumerate(errors):
# remove colons
errors[idx] = errors[idx].replace(':', '').strip()
# if not last
if idx != len(errors) - 1:
# remove last 7 chars eg: ', 3095 '
errors[idx] = errors[idx][:-6]
return errors
def log_error(self, data={}):
if not isinstance(data, dict):
data = json.loads(data)
seperator = "--" * 50
err_tb = traceback.format_exc()
err_msg = str(sys.exc_info()[1])
data = json.dumps(data, indent=4)
message = "\n".join([
"Error", err_msg, seperator,
"Data:", data, seperator,
"Exception:", err_tb
])
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
def raise_error(self, raise_exception=False, errors=[]):
title = _('E Invoice Request Failed')
if errors:
frappe.throw(errors, title=title, as_list=1)
else:
link_to_error_list = '<a href="desk#List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
frappe.msgprint(
_('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
title=title,
raise_exception=raise_exception,
indicator='red'
)
def set_einvoice_data(self, res):
enc_signed_invoice = res.get('SignedInvoice')
dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data']
self.invoice.irn = res.get('Irn')
self.invoice.ewaybill = res.get('EwbNo')
self.invoice.signed_einvoice = dec_signed_invoice
self.invoice.signed_qr_code = res.get('SignedQRCode')
self.attach_qrcode_image()
self.invoice.flags.updater_reference = {
'doctype': self.invoice.doctype,
'docname': self.invoice.name,
'label': _('IRN Generated')
}
self.update_invoice()
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
docname = self.invoice.name
_file = frappe.new_doc('File')
_file.update({
'file_name': f'QRCode_{docname}.png',
'attached_to_doctype': doctype,
'attached_to_name': docname,
'content': 'qrcode',
'is_private': 1
})
_file.insert()
frappe.db.commit()
url = qrcreate(qrcode, error='L')
abs_file_path = os.path.abspath(_file.get_full_path())
url.png(abs_file_path, scale=2, quiet_zone=1)
self.invoice.qrcode_image = _file.file_url
def update_invoice(self):
self.invoice.flags.ignore_validate_update_after_submit = True
self.invoice.flags.ignore_validate = True
self.invoice.save()
@frappe.whitelist()
def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname)
return make_einvoice(invoice)
@frappe.whitelist()
def generate_irn(doctype, docname):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.generate_irn()
@frappe.whitelist()
def cancel_irn(doctype, docname, irn, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_irn(irn, reason, remark)
@frappe.whitelist()
def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.generate_eway_bill(**kwargs)
@frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
gsp_connector = GSPConnector(doctype, docname)
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)

View File

@ -87,7 +87,7 @@ def add_custom_roles_for_reports():
)).insert() )).insert()
def add_permissions(): def add_permissions():
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'):
add_permission(doctype, 'All', 0) add_permission(doctype, 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'): for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission(doctype, role, 0) add_permission(doctype, role, 0)
@ -103,9 +103,10 @@ def add_permissions():
def add_print_formats(): def add_print_formats():
frappe.reload_doc("regional", "print_format", "gst_tax_invoice") frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
name in('GST POS Invoice', 'GST Tax Invoice') """) name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
def make_custom_fields(update=True): def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@ -351,7 +352,6 @@ def make_custom_fields(update=True):
'label': 'Mode of Transport', 'label': 'Mode of Transport',
'fieldtype': 'Select', 'fieldtype': 'Select',
'options': '\nRoad\nAir\nRail\nShip', 'options': '\nRoad\nAir\nRail\nShip',
'default': 'Road',
'insert_after': 'transporter_name', 'insert_after': 'transporter_name',
'print_hide': 1, 'print_hide': 1,
'translatable': 0 'translatable': 0
@ -388,13 +388,34 @@ def make_custom_fields(update=True):
'fieldname': 'ewaybill', 'fieldname': 'ewaybill',
'label': 'E-Way Bill No.', 'label': 'E-Way Bill No.',
'fieldtype': 'Data', 'fieldtype': 'Data',
'depends_on': 'eval:(doc.docstatus === 1)', 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)',
'allow_on_submit': 1, 'allow_on_submit': 1,
'insert_after': 'tax_id', 'insert_after': 'tax_id',
'translatable': 0 'translatable': 0
} }
] ]
si_einvoice_fields = [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
]
custom_fields = { custom_fields = {
'Address': [ 'Address': [
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data', dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
@ -407,7 +428,7 @@ def make_custom_fields(update=True):
'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields, 'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields,
'Purchase Order': purchase_invoice_gst_fields, 'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
'Sales Order': sales_invoice_gst_fields, 'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field, 'Tax Category': inter_state_gst_field,

View File

@ -74,67 +74,71 @@ frappe.query_reports["Sales Analytics"] = {
return Object.assign(options, { return Object.assign(options, {
checkboxColumn: true, checkboxColumn: true,
events: { events: {
onCheckRow: function(data) { onCheckRow: function (data) {
if (!data) return;
const data_doctype = $(
data[2].html
)[0].attributes.getNamedItem("data-doctype").value;
const tree_type = frappe.query_report.filters[0].value;
if (data_doctype != tree_type) return;
row_name = data[2].content; row_name = data[2].content;
length = data.length; length = data.length;
var tree_type = frappe.query_report.filters[0].value; if (tree_type == "Customer") {
row_values = data
if(tree_type == "Customer") { .slice(4, length - 1)
row_values = data.slice(4,length-1).map(function (column) { .map(function (column) {
return column.content; return column.content;
}) });
} else if (tree_type == "Item") { } else if (tree_type == "Item") {
row_values = data.slice(5,length-1).map(function (column) { row_values = data
return column.content; .slice(5, length - 1)
}) .map(function (column) {
} return column.content;
else { });
row_values = data.slice(3,length-1).map(function (column) { } else {
return column.content; row_values = data
}) .slice(3, length - 1)
.map(function (column) {
return column.content;
});
} }
entry = { entry = {
'name':row_name, name: row_name,
'values':row_values values: row_values,
} };
let raw_data = frappe.query_report.chart.data; let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets; let new_datasets = raw_data.datasets;
var found = false; let element_found = new_datasets.some((element, index, array)=>{
if(element.name == row_name){
for(var i=0; i < new_datasets.length;i++){ array.splice(index, 1)
if(new_datasets[i].name == row_name){ return true
found = true;
new_datasets.splice(i,1);
break;
} }
} return false
})
if(!found){ if (!element_found) {
new_datasets.push(entry); new_datasets.push(entry);
} }
let new_data = { let new_data = {
labels: raw_data.labels, labels: raw_data.labels,
datasets: new_datasets datasets: new_datasets,
} };
chart_options = {
setTimeout(() => { data: new_data,
frappe.query_report.chart.update(new_data) type: "line",
}, 500) };
frappe.query_report.render_chart(chart_options);
setTimeout(() => {
frappe.query_report.chart.draw(true);
}, 1000)
frappe.query_report.raw_chart_data = new_data; frappe.query_report.raw_chart_data = new_data;
}, },
} },
}) });
}, },
} }

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"): "Item Default", "Customer", "Supplier"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@ -15,6 +15,7 @@ frappe.ui.form.on("Delivery Note", {
'Installation Note': 'Installation Note', 'Installation Note': 'Installation Note',
'Sales Invoice': 'Invoice', 'Sales Invoice': 'Invoice',
'Stock Entry': 'Return', 'Stock Entry': 'Return',
'Shipment': 'Shipment'
}, },
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
function(doc) { function(doc) {

View File

@ -598,6 +598,9 @@ def make_shipment(source_name, target_doc=None):
pickup_contact_display += '<br>' + user.mobile_no pickup_contact_display += '<br>' + user.mobile_no
target.pickup_contact = pickup_contact_display target.pickup_contact = pickup_contact_display
# As we are using session user details in the pickup_contact then pickup_contact_person will be session user
target.pickup_contact_person = frappe.session.user
contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1) contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1)
delivery_contact_display = '{}'.format(source.contact_display) delivery_contact_display = '{}'.format(source.contact_display)
if contact: if contact:
@ -609,6 +612,13 @@ def make_shipment(source_name, target_doc=None):
delivery_contact_display += '<br>' + contact.mobile_no delivery_contact_display += '<br>' + contact.mobile_no
target.delivery_contact = delivery_contact_display target.delivery_contact = delivery_contact_display
if source.shipping_address_name:
target.delivery_address_name = source.shipping_address_name
target.delivery_address = source.shipping_address
elif source.customer_address:
target.delivery_address_name = source.customer_address
target.delivery_address = source.address_display
doclist = get_mapped_doc("Delivery Note", source_name, { doclist = get_mapped_doc("Delivery Note", source_name, {
"Delivery Note": { "Delivery Note": {
"doctype": "Shipment", "doctype": "Shipment",
@ -617,9 +627,7 @@ def make_shipment(source_name, target_doc=None):
"company": "pickup_company", "company": "pickup_company",
"company_address": "pickup_address_name", "company_address": "pickup_address_name",
"company_address_display": "pickup_address", "company_address_display": "pickup_address",
"address_display": "delivery_address",
"customer": "delivery_customer", "customer": "delivery_customer",
"shipping_address_name": "delivery_address_name",
"contact_person": "delivery_contact_name", "contact_person": "delivery_contact_name",
"contact_email": "delivery_contact_email" "contact_email": "delivery_contact_email"
}, },

View File

@ -19,7 +19,7 @@ def get_data():
}, },
{ {
'label': _('Reference'), 'label': _('Reference'),
'items': ['Sales Order', 'Quality Inspection'] 'items': ['Sales Order', 'Shipment', 'Quality Inspection']
}, },
{ {
'label': _('Returns'), 'label': _('Returns'),

View File

@ -325,7 +325,7 @@ class PurchaseReceipt(BuyingController):
elif d.warehouse not in warehouse_with_no_account or \ elif d.warehouse not in warehouse_with_no_account or \
d.rejected_warehouse not in warehouse_with_no_account: d.rejected_warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(d.warehouse) warehouse_with_no_account.append(d.warehouse)
elif d.item_code not in stock_items and flt(d.qty) and auto_accounting_for_non_stock_items: elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
credit_currency = get_account_currency(service_received_but_not_billed_account) credit_currency = get_account_currency(service_received_but_not_billed_account)

View File

@ -575,7 +575,7 @@ class TestPurchaseReceipt(unittest.TestCase):
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1, se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1,
serial_no=serial_no, basic_rate=100, do_not_submit=True) serial_no=serial_no, basic_rate=100, do_not_submit=True)
self.assertRaises(SerialNoDuplicateError, se.submit) se.submit()
dn.cancel() dn.cancel()
pr1.cancel() pr1.cancel()

View File

@ -4,6 +4,11 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type; cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", { frappe.ui.form.on("Quality Inspection", {
refresh: function(frm) {
// Ignore cancellation of reference doctype on cancel all.
frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
},
item_code: function(frm) { item_code: function(frm) {
if (frm.doc.item_code) { if (frm.doc.item_code) {
return frm.call({ return frm.call({

View File

@ -5,11 +5,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint, get_link_to_form
from erpnext.stock.stock_ledger import repost_future_sle from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.accounts.utils import update_gl_entries_after from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
from frappe.utils.user import get_users_with_role
from frappe import _
class RepostItemValuation(Document): class RepostItemValuation(Document):
def validate(self): def validate(self):
self.set_status() self.set_status()
@ -51,12 +51,20 @@ def repost(doc):
repost_sl_entries(doc) repost_sl_entries(doc)
repost_gl_entries(doc) repost_gl_entries(doc)
check_if_stock_and_account_balance_synced(doc.posting_date, doc.company)
doc.set_status('Completed') doc.set_status('Completed')
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(traceback) frappe.log_error(traceback)
frappe.db.set_value(doc.doctype, doc.name, 'error_log', traceback)
message = frappe.message_log.pop()
if traceback:
message += "<br>" + "Traceback: <br>" + traceback
frappe.db.set_value(doc.doctype, doc.name, 'error_log', message)
notify_error_to_stock_managers(doc)
doc.set_status('Failed') doc.set_status('Failed')
raise raise
finally: finally:
@ -87,3 +95,18 @@ def repost_gl_entries(doc):
update_gl_entries_after(doc.posting_date, doc.posting_time, update_gl_entries_after(doc.posting_date, doc.posting_time,
warehouses, items, company=doc.company) warehouses, items, company=doc.company)
def notify_error_to_stock_managers(doc, traceback):
recipients = get_users_with_role("Stock Manager")
if not recipients:
get_users_with_role("System Manager")
subject = _("Error while reposting item valuation")
message = (_("Hi,") + "<br>"
+ _("An error has been appeared while reposting item valuation via {0}")
.format(get_link_to_form(doc.doctype, doc.name)) + "<br>"
+ _("Please check the error message and take necessary actions to fix the error and then restart the reposting again.")
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)

View File

@ -6,7 +6,7 @@ import frappe
import json import json
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate, get_link_to_form
from erpnext.stock.get_item_details import get_reserved_qty_for_so from erpnext.stock.get_item_details import get_reserved_qty_for_so
from frappe import _, ValidationError from frappe import _, ValidationError
@ -241,7 +241,7 @@ def validate_serial_no(sle, item_det):
for serial_no in serial_nos: for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no): if frappe.db.exists("Serial No", serial_no):
sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order", sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order",
"delivery_document_no", "delivery_document_type", "warehouse", "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type",
"purchase_document_no", "company"], as_dict=1) "purchase_document_no", "company"], as_dict=1)
if sr.item_code!=sle.item_code: if sr.item_code!=sle.item_code:
@ -249,9 +249,10 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no, frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
sle.item_code), SerialNoItemError) sle.item_code), SerialNoItemError)
if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle): if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
frappe.throw(_("Serial No {0} has already been received").format(serial_no), doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
SerialNoDuplicateError) frappe.throw(_("Serial No {0} has already been received in the {1} #{2}")
.format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError)
if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation'] if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation']
and sle.voucher_type == sr.delivery_document_type): and sle.voucher_type == sr.delivery_document_type):
@ -348,7 +349,7 @@ def validate_so_serial_no(sr, sales_order):
frappe.throw(_("""{0} Serial No {1} cannot be delivered""") frappe.throw(_("""{0} Serial No {1} cannot be delivered""")
.format(msg, sr.name)) .format(msg, sr.name))
def has_duplicate_serial_no(sn, sle): def has_serial_no_exists(sn, sle):
if (sn.warehouse and not sle.skip_serial_no_validaiton if (sn.warehouse and not sle.skip_serial_no_validaiton
and sle.voucher_type != 'Stock Reconciliation'): and sle.voucher_type != 'Stock Reconciliation'):
return True return True
@ -358,12 +359,13 @@ def has_duplicate_serial_no(sn, sle):
status = False status = False
if sn.purchase_document_no: if sn.purchase_document_no:
if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \ if (sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and
sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]: sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]):
status = True status = True
if status and sle.voucher_type == 'Stock Entry' and \ # If status is receipt then system will allow to in-ward the delivered serial no
frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') != 'Material Receipt': if (status and sle.voucher_type == "Stock Entry" and frappe.db.get_value("Stock Entry",
sle.voucher_no, "purpose") in ("Material Receipt", "Material Transfer")):
status = False status = False
return status return status
@ -419,7 +421,7 @@ def auto_make_serial_nos(args):
if is_new: if is_new:
created_numbers.append(sr.name) created_numbers.append(sr.name)
form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers)) form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers))
# Setting up tranlated title field for all cases # Setting up tranlated title field for all cases
singular_title = _("Serial Number Created") singular_title = _("Serial Number Created")

View File

@ -345,7 +345,8 @@
"label": "Status", "label": "Status",
"no_copy": 1, "no_copy": 1,
"options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted", "options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"fieldname": "tracking_url", "fieldname": "tracking_url",
@ -430,7 +431,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-02 15:43:44.607039", "modified": "2020-12-25 15:02:34.891976",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Shipment", "name": "Shipment",

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt, get_time
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.party import get_party_shipping_address
from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.contacts.doctype.contact.contact import get_default_contact
@ -13,6 +13,7 @@ from frappe.contacts.doctype.contact.contact import get_default_contact
class Shipment(Document): class Shipment(Document):
def validate(self): def validate(self):
self.validate_weight() self.validate_weight()
self.validate_pickup_time()
self.set_value_of_goods() self.set_value_of_goods()
if self.docstatus == 0: if self.docstatus == 0:
self.status = 'Draft' self.status = 'Draft'
@ -32,6 +33,10 @@ class Shipment(Document):
if flt(parcel.weight) <= 0: if flt(parcel.weight) <= 0:
frappe.throw(_('Parcel weight cannot be 0')) frappe.throw(_('Parcel weight cannot be 0'))
def validate_pickup_time(self):
if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from):
frappe.throw(_("Pickup To time should be greater than Pickup From time"))
def set_value_of_goods(self): def set_value_of_goods(self):
value_of_goods = 0 value_of_goods = 0
for entry in self.get("shipment_delivery_note"): for entry in self.get("shipment_delivery_note"):

View File

@ -259,11 +259,16 @@ class StockEntry(StockController):
item_code.append(item.item_code) item_code.append(item.item_code)
def validate_fg_completed_qty(self): def validate_fg_completed_qty(self):
item_wise_qty = {}
if self.purpose == "Manufacture" and self.work_order: if self.purpose == "Manufacture" and self.work_order:
for d in self.items: for d in self.items:
if d.is_finished_item and d.qty != self.fg_completed_qty: if d.is_finished_item:
frappe.throw(_("Finished product quantity <b>{0}</b> and For Quantity <b>{1}</b> cannot be different") item_wise_qty.setdefault(d.item_code, []).append(d.qty)
.format(d.qty, self.fg_completed_qty))
for item_code, qty_list in iteritems(item_wise_qty):
if self.fg_completed_qty != sum(qty_list):
frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different")
.format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty)))
def validate_difference_account(self): def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@ -319,7 +324,7 @@ class StockEntry(StockController):
if self.purpose == "Manufacture": if self.purpose == "Manufacture":
if validate_for_manufacture: if validate_for_manufacture:
if d.bom_no: if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None d.s_warehouse = None
if not d.t_warehouse: if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx)) frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
@ -442,6 +447,7 @@ class StockEntry(StockController):
""" """
# Set rate for outgoing items # Set rate for outgoing items
outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate) outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate)
finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item])
# Set basic rate for incoming items # Set basic rate for incoming items
for d in self.get('items'): for d in self.get('items'):
@ -451,7 +457,7 @@ class StockEntry(StockController):
d.basic_rate = 0.0 d.basic_rate = 0.0
elif d.is_finished_item: elif d.is_finished_item:
if self.purpose == "Manufacture": if self.purpose == "Manufacture":
d.basic_rate = self.get_basic_rate_for_manufactured_item(d.transfer_qty, outgoing_items_cost) d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost)
elif self.purpose == "Repack": elif self.purpose == "Repack":
d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost) d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost)
@ -511,7 +517,7 @@ class StockEntry(StockController):
bom_items = self.get_bom_raw_materials(finished_item_qty) bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
return flt(outgoing_items_cost - scrap_items_cost) return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)
def distribute_additional_costs(self): def distribute_additional_costs(self):
# If no incoming items, set additional costs blank # If no incoming items, set additional costs blank
@ -666,7 +672,7 @@ class StockEntry(StockController):
production_item, wo_qty = frappe.db.get_value("Work Order", production_item, wo_qty = frappe.db.get_value("Work Order",
self.work_order, ["production_item", "qty"]) self.work_order, ["production_item", "qty"])
number_of_finished_items = 0 finished_items = []
for d in self.get('items'): for d in self.get('items'):
if d.is_finished_item: if d.is_finished_item:
if d.item_code != production_item: if d.item_code != production_item:
@ -675,9 +681,9 @@ class StockEntry(StockController):
elif flt(d.transfer_qty) > flt(self.fg_completed_qty): elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
format(d.idx, d.transfer_qty, self.fg_completed_qty)) format(d.idx, d.transfer_qty, self.fg_completed_qty))
number_of_finished_items += 1 finished_items.append(d.item_code)
if number_of_finished_items > 1: if len(set(finished_items)) > 1:
frappe.throw(_("Multiple items cannot be marked as finished item")) frappe.throw(_("Multiple items cannot be marked as finished item"))
if self.purpose == "Manufacture": if self.purpose == "Manufacture":

View File

@ -179,22 +179,20 @@ class TestStockEntry(unittest.TestCase):
def test_material_transfer_gl_entry(self): def test_material_transfer_gl_entry(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
create_stock_reconciliation(qty=100, rate=100)
mtn = make_stock_entry(item_code="_Test Item", source="Stores - TCP1", mtn = make_stock_entry(item_code="_Test Item", source="Stores - TCP1",
target="Finished Goods - TCP1", qty=45) target="Finished Goods - TCP1", qty=45, company=company)
self.check_stock_ledger_entries("Stock Entry", mtn.name, self.check_stock_ledger_entries("Stock Entry", mtn.name,
[["_Test Item", "Stores - TCP1", -45.0], ["_Test Item", "Finished Goods - TCP1", 45.0]]) [["_Test Item", "Stores - TCP1", -45.0], ["_Test Item", "Finished Goods - TCP1", 45.0]])
stock_in_hand_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse) source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse)
fixed_asset_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse) target_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse)
if stock_in_hand_account == fixed_asset_account: if source_warehouse_account == target_warehouse_account:
# no gl entry as both source and target warehouse has linked to same account. # no gl entry as both source and target warehouse has linked to same account.
self.assertFalse(frappe.db.sql("""select * from `tabGL Entry` self.assertFalse(frappe.db.sql("""select * from `tabGL Entry`
where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name)) where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name, as_dict=1))
else: else:
stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry", stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry",
@ -202,8 +200,8 @@ class TestStockEntry(unittest.TestCase):
self.check_gl_entries("Stock Entry", mtn.name, self.check_gl_entries("Stock Entry", mtn.name,
sorted([ sorted([
[stock_in_hand_account, 0.0, stock_value_diff], [source_warehouse_account, 0.0, stock_value_diff],
[fixed_asset_account, stock_value_diff, 0.0], [target_warehouse_account, stock_value_diff, 0.0],
]) ])
) )
@ -754,37 +752,37 @@ class TestStockEntry(unittest.TestCase):
def test_total_basic_amount_zero(self): def test_total_basic_amount_zero(self):
se = frappe.get_doc({"doctype":"Stock Entry", se = frappe.get_doc({"doctype":"Stock Entry",
"purpose":"Material Receipt", "purpose":"Material Receipt",
"stock_entry_type":"Material Receipt", "stock_entry_type":"Material Receipt",
"posting_date": nowdate(), "posting_date": nowdate(),
"company":"_Test Company with perpetual inventory", "company":"_Test Company with perpetual inventory",
"items":[ "items":[
{ {
"item_code":"_Test Item", "item_code":"_Test Item",
"description":"_Test Item", "description":"_Test Item",
"qty": 1, "qty": 1,
"basic_rate": 0, "basic_rate": 0,
"uom":"Nos", "uom":"Nos",
"t_warehouse": "Stores - TCP1", "t_warehouse": "Stores - TCP1",
"allow_zero_valuation_rate": 1, "allow_zero_valuation_rate": 1,
"cost_center": "Main - TCP1" "cost_center": "Main - TCP1"
}, },
{ {
"item_code":"_Test Item", "item_code":"_Test Item",
"description":"_Test Item", "description":"_Test Item",
"qty": 2, "qty": 2,
"basic_rate": 0, "basic_rate": 0,
"uom":"Nos", "uom":"Nos",
"t_warehouse": "Stores - TCP1", "t_warehouse": "Stores - TCP1",
"allow_zero_valuation_rate": 1, "allow_zero_valuation_rate": 1,
"cost_center": "Main - TCP1" "cost_center": "Main - TCP1"
}, },
], ],
"additional_costs":[ "additional_costs":[
{"expense_account":"Miscellaneous Expenses - TCP1", {"expense_account":"Miscellaneous Expenses - TCP1",
"amount":100, "amount":100,
"description": "miscellanous"} "description": "miscellanous"
] }]
}) })
se.insert() se.insert()
se.submit() se.submit()

View File

@ -526,7 +526,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-09-23 17:55:03.384138", "modified": "2020-12-23 17:55:03.384138",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",

View File

@ -217,7 +217,7 @@
"fieldname": "role_allowed_to_create_edit_back_dated_transactions", "fieldname": "role_allowed_to_create_edit_back_dated_transactions",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Role Allowed to Create/Edit Back-dated Transactions", "label": "Role Allowed to Create/Edit Back-dated Transactions",
"options": "User" "options": "Role"
}, },
{ {
"fieldname": "column_break_26", "fieldname": "column_break_26",
@ -234,7 +234,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-11-23 22:26:54.225608", "modified": "2020-12-29 12:53:31.162247",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@ -74,7 +74,9 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
update_party_blanket_order(args, out) update_party_blanket_order(args, out)
get_price_list_rate(args, item, out) if not doc or cint(doc.get('is_return')) == 0:
# get price list rate only if the invoice is not a credit or debit note
get_price_list_rate(args, item, out)
if args.customer and cint(args.is_pos): if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args)) out.update(get_pos_profile_item_details(args.company, args))

Some files were not shown because too many files have changed in this diff Show More