Merge branch 'develop' into validate_regional_germany

This commit is contained in:
Raffael Meyer 2020-12-29 11:58:57 +01:00 committed by GitHub
commit a3462f6b0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
113 changed files with 5674 additions and 1350 deletions

View File

@ -172,7 +172,7 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", doc) frappe.delete_doc("Account", doc)
def _make_test_records(verbose): def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
accounts = [ accounts = [

View File

@ -28,22 +28,22 @@ def test_create_test_data():
"item_group": "_Test Item Group", "item_group": "_Test Item Group",
"item_name": "_Test Tesla Car", "item_name": "_Test Tesla Car",
"apply_warehouse_wise_reorder_level": 0, "apply_warehouse_wise_reorder_level": 0,
"warehouse":"Stores - TCP1", "warehouse":"Stores - _TC",
"gst_hsn_code": "999800", "gst_hsn_code": "999800",
"valuation_rate": 5000, "valuation_rate": 5000,
"standard_rate":5000, "standard_rate":5000,
"item_defaults": [{ "item_defaults": [{
"company": "_Test Company with perpetual inventory", "company": "_Test Company",
"default_warehouse": "Stores - TCP1", "default_warehouse": "Stores - _TC",
"default_price_list":"_Test Price List", "default_price_list":"_Test Price List",
"expense_account": "Cost of Goods Sold - TCP1", "expense_account": "Cost of Goods Sold - _TC",
"buying_cost_center": "Main - TCP1", "buying_cost_center": "Main - _TC",
"selling_cost_center": "Main - TCP1", "selling_cost_center": "Main - _TC",
"income_account": "Sales - TCP1" "income_account": "Sales - _TC"
}], }],
"show_in_website": 1, "show_in_website": 1,
"route":"-test-tesla-car", "route":"-test-tesla-car",
"website_warehouse": "Stores - TCP1" "website_warehouse": "Stores - _TC"
}) })
item.insert() item.insert()
# create test item price # create test item price
@ -65,12 +65,12 @@ def test_create_test_data():
"items": [{ "items": [{
"item_code": "_Test Tesla Car" "item_code": "_Test Tesla Car"
}], }],
"warehouse":"Stores - TCP1", "warehouse":"Stores - _TC",
"coupon_code_based":1, "coupon_code_based":1,
"selling": 1, "selling": 1,
"rate_or_discount": "Discount Percentage", "rate_or_discount": "Discount Percentage",
"discount_percentage": 30, "discount_percentage": 30,
"company": "_Test Company with perpetual inventory", "company": "_Test Company",
"currency":"INR", "currency":"INR",
"for_price_list":"_Test Price List" "for_price_list":"_Test Price List"
}) })
@ -85,7 +85,7 @@ def test_create_test_data():
}) })
sales_partner.insert() sales_partner.insert()
# create test item coupon code # create test item coupon code
if not frappe.db.exists("Coupon Code","SAVE30"): if not frappe.db.exists("Coupon Code", "SAVE30"):
coupon_code = frappe.get_doc({ coupon_code = frappe.get_doc({
"doctype": "Coupon Code", "doctype": "Coupon Code",
"coupon_name":"SAVE30", "coupon_name":"SAVE30",
@ -104,33 +104,25 @@ class TestCouponCode(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
def test_1_check_coupon_code_used_before_so(self): def test_sales_order_with_coupon_code(self):
coupon_code = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"})) frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
# reset used coupon code count
coupon_code.used=0
coupon_code.save()
# check no coupon code is used before sales order is made
self.assertEqual(coupon_code.get("used"),0)
def test_2_sales_order_with_coupon_code(self): so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', customer="_Test Customer", selling_price_list="_Test Price List",
customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1, item_code="_Test Tesla Car", rate=5000, qty=1,
do_not_submit=True) do_not_submit=True)
so = frappe.get_doc('Sales Order', so.name)
# check item price before coupon code is applied
self.assertEqual(so.items[0].rate, 5000) self.assertEqual(so.items[0].rate, 5000)
so.coupon_code='SAVE30' so.coupon_code='SAVE30'
so.sales_partner='_Test Coupon Partner' so.sales_partner='_Test Coupon Partner'
so.save() so.save()
# check item price after coupon code is applied # check item price after coupon code is applied
self.assertEqual(so.items[0].rate, 3500) self.assertEqual(so.items[0].rate, 3500)
so.submit() so.submit()
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)
def test_3_check_coupon_code_used_after_so(self):
doc = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
# check no coupon code is used before sales order is made
self.assertEqual(doc.get("used"),1)

View File

@ -30,20 +30,22 @@ class GLEntry(Document):
self.pl_must_have_cost_center() self.pl_must_have_cost_center()
self.validate_cost_center() self.validate_cost_center()
self.check_pl_account() if not self.flags.from_repost:
self.validate_party() self.check_pl_account()
self.validate_currency() self.validate_party()
self.validate_currency()
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'): def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
self.validate_account_details(adv_adj) if not from_repost:
self.validate_dimensions_for_pl_and_bs() self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
validate_frozen_account(self.account, adv_adj) validate_frozen_account(self.account, adv_adj)
validate_balance_type(self.account, adv_adj) validate_balance_type(self.account, adv_adj)
# Update outstanding amt on against voucher # Update outstanding amt on against voucher
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \ if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
and self.against_voucher and update_outstanding == 'Yes': and self.against_voucher and update_outstanding == 'Yes' and not from_repost:
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher) self.against_voucher)
@ -106,8 +108,8 @@ class GLEntry(Document):
from tabAccount where name=%s""", self.account, as_dict=1)[0] from tabAccount where name=%s""", self.account, as_dict=1)[0]
if ret.is_group==1: if ret.is_group==1:
frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
transactions''').format(self.voucher_type, self.voucher_no, self.account)) .format(self.voucher_type, self.voucher_no, self.account))
if ret.docstatus==2: if ret.docstatus==2:
frappe.throw(_("{0} {1}: Account {2} is inactive") frappe.throw(_("{0} {1}: Account {2} is inactive")
@ -136,8 +138,8 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company)) .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
if self.cost_center and _check_is_group(): if self.cost_center and _check_is_group():
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""")
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))) .format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self): def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party) validate_party_frozen_disabled(self.party_type, self.party)

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):
@ -75,54 +75,46 @@ class TestJournalEntry(unittest.TestCase):
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]: elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher # if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
frappe.db.set_value("Accounts Settings", "Accounts Settings",
"unlink_advance_payment_on_cancelation_of_order", 0)
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name) submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel) self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
def test_jv_against_stock_account(self): def test_jv_against_stock_account(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory company = "_Test Company with perpetual inventory"
set_perpetual_inventory() stock_account = get_inventory_account(company)
jv = frappe.copy_doc({ from erpnext.accounts.utils import get_stock_and_account_balance
"cheque_date": nowdate(), account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
"cheque_no": "33", diff = flt(account_bal) - flt(stock_bal)
"company": "_Test Company with perpetual inventory",
"doctype": "Journal Entry",
"accounts": [
{
"account": "Debtors - TCP1",
"party_type": "Customer",
"party": "_Test Customer",
"credit_in_account_currency": 400.0,
"debit_in_account_currency": 0.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "Main - TCP1"
},
{
"account": "_Test Bank - TCP1",
"credit_in_account_currency": 0.0,
"debit_in_account_currency": 400.0,
"doctype": "Journal Entry Account",
"parentfield": "accounts",
"cost_center": "Main - TCP1"
}
],
"naming_series": "_T-Journal Entry-",
"posting_date": nowdate(),
"user_remark": "test",
"voucher_type": "Bank Entry"
})
jv.get("accounts")[0].update({ if not diff:
"account": get_inventory_account('_Test Company with perpetual inventory'), diff = 100
"company": "_Test Company with perpetual inventory",
"party_type": None, jv = frappe.new_doc("Journal Entry")
"party": None jv.company = company
jv.posting_date = nowdate()
jv.append("accounts", {
"account": stock_account,
"cost_center": "Main - TCP1",
"debit_in_account_currency": 0 if diff > 0 else abs(diff),
"credit_in_account_currency": diff if diff > 0 else 0
}) })
self.assertRaises(StockAccountInvalidTransaction, jv.submit) jv.append("accounts", {
jv.cancel() "account": "Stock Adjustment - TCP1",
set_perpetual_inventory(0) "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()
if account_bal == stock_bal:
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
frappe.db.rollback()
else:
jv.submit()
jv.cancel()
def test_multi_currency(self): def test_multi_currency(self):
jv = make_journal_entry("_Test Bank USD - _TC", jv = make_journal_entry("_Test Bank USD - _TC",

View File

@ -8,12 +8,10 @@ import unittest
from frappe.utils import today, cint, flt, getdate from frappe.utils import today, cint, flt, getdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
from erpnext.accounts.party import get_dashboard_info from erpnext.accounts.party import get_dashboard_info
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestLoyaltyProgram(unittest.TestCase): class TestLoyaltyProgram(unittest.TestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
set_perpetual_inventory(0)
# create relevant item, customer, loyalty program, etc # create relevant item, customer, loyalty program, etc
create_records() create_records()

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

@ -410,10 +410,13 @@ class PurchaseInvoice(BuyingController):
# this sequence because outstanding may get -negative # this sequence because outstanding may get -negative
self.make_gl_entries() self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_project() self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
def make_gl_entries(self, gl_entries=None): def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries: if not gl_entries:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
@ -421,7 +424,7 @@ class PurchaseInvoice(BuyingController):
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1: if self.docstatus == 1:
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False) make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2: elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@ -436,9 +439,11 @@ class PurchaseInvoice(BuyingController):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock: if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
else: else:
self.stock_received_but_not_billed = None self.stock_received_but_not_billed = None
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") self.expenses_included_in_valuation = None
self.negative_expense_to_be_booked = 0.0 self.negative_expense_to_be_booked = 0.0
gl_entries = [] gl_entries = []
@ -994,11 +999,15 @@ class PurchaseInvoice(BuyingController):
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_project() self.update_project()
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_project(self): def update_project(self):
project_list = [] project_list = []

View File

@ -9,8 +9,7 @@ import frappe.model
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import cint, flt, today, nowdate, add_days, getdate from frappe.utils import cint, flt, today, nowdate, add_days, getdate
import frappe.defaults import frappe.defaults
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes
test_records as pr_test_records, make_purchase_receipt, get_taxes
from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
@ -33,13 +32,10 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_gl_entries_without_perpetual_inventory(self): def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
wrapper = frappe.copy_doc(test_records[0]) pi = frappe.copy_doc(test_records[0])
set_perpetual_inventory(0, wrapper.company) self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company)))
self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company))) pi.insert()
wrapper.insert() pi.submit()
wrapper.submit()
wrapper.load_from_db()
dl = wrapper
expected_gl_entries = { expected_gl_entries = {
"_Test Payable - _TC": [0, 1512.0], "_Test Payable - _TC": [0, 1512.0],
@ -54,12 +50,16 @@ class TestPurchaseInvoice(unittest.TestCase):
"Round Off - _TC": [0, 0.3] "Round Off - _TC": [0, 0.3]
} }
gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry` gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
where voucher_type = 'Purchase Invoice' and voucher_no = %s""", dl.name, as_dict=1) where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1)
for d in gl_entries: for d in gl_entries:
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account)) self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
def test_gl_entries_with_perpetual_inventory(self): def test_gl_entries_with_perpetual_inventory(self):
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10) pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
expense_account ="_Test Account Cost for Goods Sold - TCP1",
get_taxes_and_charges=True, qty=10)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1) self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
self.check_gle_for_pi(pi.name) self.check_gle_for_pi(pi.name)
@ -198,8 +198,6 @@ class TestPurchaseInvoice(unittest.TestCase):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,) pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True") pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
for d in pi.items: for d in pi.items:
@ -247,17 +245,11 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertRaises(frappe.CannotChangeConstantError, pi.save) self.assertRaises(frappe.CannotChangeConstantError, pi.save)
def test_gl_entries_with_aia_for_non_stock_items(self): def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
pi = frappe.copy_doc(test_records[1]) pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
set_perpetual_inventory(1, pi.company) company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1) cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
pi.get("items")[0].item_code = "_Test Non Stock Item"
pi.get("items")[0].expense_account = "_Test Account Cost for Goods Sold - _TC"
pi.get("taxes").pop(0)
pi.get("taxes").pop(1)
pi.insert()
pi.submit()
pi.load_from_db()
self.assertTrue(pi.status, "Unpaid") self.assertTrue(pi.status, "Unpaid")
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, debit, credit
@ -265,17 +257,15 @@ class TestPurchaseInvoice(unittest.TestCase):
order by account asc""", pi.name, as_dict=1) order by account asc""", pi.name, as_dict=1)
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = sorted([ expected_values = [
["_Test Payable - _TC", 0, 620], ["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
["_Test Account Cost for Goods Sold - _TC", 500.0, 0], ["Creditors - TCP1", 0, 250]
["_Test Account VAT - _TC", 120.0, 0], ]
])
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account) self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit) self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit) self.assertEqual(expected_values[i][2], gle.credit)
set_perpetual_inventory(0, pi.company)
def test_purchase_invoice_calculation(self): def test_purchase_invoice_calculation(self):
pi = frappe.copy_doc(test_records[0]) pi = frappe.copy_doc(test_records[0])
@ -457,12 +447,13 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.cancel() pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost) self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
def test_return_purchase_invoice(self): def test_return_purchase_invoice_with_perpetual_inventory(self):
set_perpetual_inventory() pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
pi = make_purchase_invoice() return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2) cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
# check gl entries for return # check gl entries for return
@ -473,19 +464,15 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = { expected_values = {
"Creditors - _TC": [100.0, 0.0], "Creditors - TCP1": [100.0, 0.0],
"Stock Received But Not Billed - _TC": [0.0, 100.0], "Stock Received But Not Billed - TCP1": [0.0, 100.0],
} }
for gle in gl_entries: for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit) self.assertEqual(expected_values[gle.account][1], gle.credit)
set_perpetual_inventory(0)
def test_multi_currency_gle(self): def test_multi_currency_gle(self):
set_perpetual_inventory(0)
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
@ -640,10 +627,9 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2) self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")]) rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self): def test_rejected_serial_no(self):
set_perpetual_inventory(0)
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1, pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1, rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC") rejected_warehouse = "_Test Rejected Warehouse - _TC")

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

@ -180,6 +180,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve # this sequence because outstanding may get -ve
self.make_gl_entries() self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if not self.is_return: if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note") self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order") self.update_billing_status_for_zero_amount_refdoc("Sales Order")
@ -229,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()
@ -258,6 +261,10 @@ class SalesInvoice(SellingController):
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@ -279,7 +286,7 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains: if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel") manage_invoice_submit_cancel(self, "on_cancel")
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self): def update_status_updater_args(self):
if cint(self.update_stock): if cint(self.update_stock):
@ -722,22 +729,20 @@ class SalesInvoice(SellingController):
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1: if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note)) throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def make_gl_entries(self, gl_entries=None): def make_gl_entries(self, gl_entries=None, from_repost=False):
from erpnext.accounts.general_ledger import make_reverse_gl_entries from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not gl_entries: if not gl_entries:
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
if gl_entries: if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
# if POS and amount is written off, updating outstanding amt after posting all gl entries # if POS and amount is written off, updating outstanding amt after posting all gl entries
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
cint(self.redeem_loyalty_points)) else "Yes" cint(self.redeem_loyalty_points)) else "Yes"
if self.docstatus == 1: if self.docstatus == 1:
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False) make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2: elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)

View File

@ -17,7 +17,8 @@
"description": "138-CMS Shoe", "description": "138-CMS Shoe",
"doctype": "Sales Invoice Item", "doctype": "Sales Invoice Item",
"income_account": "Sales - _TC", "income_account": "Sales - _TC",
"expense_account": "_Test Account Cost for Goods Sold - _TC", "expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "138-CMS Shoe",
"item_name": "138-CMS Shoe", "item_name": "138-CMS Shoe",
"parentfield": "items", "parentfield": "items",
"qty": 1.0, "qty": 1.0,

View File

@ -10,7 +10,6 @@ from frappe.model.dynamic_links import get_dynamic_link_map
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
@ -659,7 +658,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_invoice_gl_entry_without_perpetual_inventory(self): def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
si = frappe.copy_doc(test_records[1]) si = frappe.copy_doc(test_records[1])
set_perpetual_inventory(0, si.company)
si.insert() si.insert()
si.submit() si.submit()
@ -815,7 +813,6 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
def test_pos_si_without_payment(self): def test_pos_si_without_payment(self):
set_perpetual_inventory()
make_pos_profile() make_pos_profile()
pos = copy.deepcopy(test_records[1]) pos = copy.deepcopy(test_records[1])
@ -829,9 +826,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, si.submit) self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self): def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
set_perpetual_inventory() si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1",
income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True)
si = frappe.get_doc(test_records[1])
si.get("items")[0].item_code = None si.get("items")[0].item_code = None
si.insert() si.insert()
si.submit() si.submit()
@ -842,24 +838,16 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [ expected_values = dict((d[0], d) for d in [
[si.debit_to, 630.0, 0.0], ["Debtors - TCP1", 100.0, 0.0],
[test_records[1]["items"][0]["income_account"], 0.0, 500.0], ["Sales - TCP1", 0.0, 100.0]
[test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
[test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
]) ])
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
set_perpetual_inventory(0)
def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self): def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self):
set_perpetual_inventory() si = create_sales_invoice(item="_Test Non Stock Item")
si = frappe.get_doc(test_records[1])
si.get("items")[0].item_code = "_Test Non Stock Item"
si.insert()
si.submit()
gl_entries = frappe.db.sql("""select account, debit, credit gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
@ -867,17 +855,14 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gl_entries) self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [ expected_values = dict((d[0], d) for d in [
[si.debit_to, 630.0, 0.0], [si.debit_to, 100.0, 0.0],
[test_records[1]["items"][0]["income_account"], 0.0, 500.0], [test_records[1]["items"][0]["income_account"], 0.0, 100.0]
[test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
[test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
]) ])
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
set_perpetual_inventory(0)
def _insert_purchase_receipt(self): def _insert_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \
@ -1106,7 +1091,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.grand_total, 859.43) self.assertEqual(si.grand_total, 859.43)
def test_multi_currency_gle(self): def test_multi_currency_gle(self):
set_perpetual_inventory(0)
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50) currency="USD", conversion_rate=50)
@ -1776,153 +1760,72 @@ class TestSalesInvoice(unittest.TestCase):
si.submit() si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name) target_doc = make_inter_company_transaction("Sales Invoice", si.name)
target_doc.items[0].update({
"expense_account": "Cost of Goods Sold - _TC1",
"cost_center": "Main - _TC1",
"warehouse": "Stores - _TC1"
})
target_doc.submit() target_doc.submit()
self.assertEqual(target_doc.company, "_Test Company 1") self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier") self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
def test_internal_transfer_gl_entry(self): # def test_internal_transfer_gl_entry(self):
## Create internal transfer account # ## Create internal transfer account
account = create_account(account_name="Unrealized Profit", # account = create_account(account_name="Unrealized Profit",
parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") # parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
frappe.db.set_value('Company', '_Test Company with perpetual inventory', # frappe.db.set_value('Company', '_Test Company with perpetual inventory',
'unrealized_profit_loss_account', account) # 'unrealized_profit_loss_account', account)
customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", # customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
"_Test Company with perpetual inventory") # "_Test Company with perpetual inventory")
create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", # create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
"_Test Company with perpetual inventory") # "_Test Company with perpetual inventory")
si = create_sales_invoice( # si = create_sales_invoice(
company = "_Test Company with perpetual inventory", # company = "_Test Company with perpetual inventory",
customer = customer, # customer = customer,
debit_to = "Debtors - TCP1", # debit_to = "Debtors - TCP1",
warehouse = "Stores - TCP1", # warehouse = "Stores - TCP1",
income_account = "Sales - TCP1", # income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", # expense_account = "Cost of Goods Sold - TCP1",
cost_center = "Main - TCP1", # cost_center = "Main - TCP1",
currency = "INR", # currency = "INR",
do_not_save = 1 # do_not_save = 1
) # )
si.selling_price_list = "_Test Price List Rest of the World" # si.selling_price_list = "_Test Price List Rest of the World"
si.update_stock = 1 # si.update_stock = 1
si.items[0].target_warehouse = 'Work In Progress - TCP1' # si.items[0].target_warehouse = 'Work In Progress - TCP1'
add_taxes(si) # add_taxes(si)
si.save() # si.save()
si.submit() # si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name) # target_doc = make_inter_company_transaction("Sales Invoice", si.name)
target_doc.company = '_Test Company with perpetual inventory' # target_doc.company = '_Test Company with perpetual inventory'
target_doc.items[0].warehouse = 'Finished Goods - TCP1' # target_doc.items[0].warehouse = 'Finished Goods - TCP1'
add_taxes(target_doc) # add_taxes(target_doc)
target_doc.save() # target_doc.save()
target_doc.submit() # target_doc.submit()
si_gl_entries = [ # si_gl_entries = [
["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()], # ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()] # ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
] # ]
check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) # check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
pi_gl_entries = [ # pi_gl_entries = [
["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()], # ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()] # ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
] # ]
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()
@ -1939,6 +1842,187 @@ 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": 2,
"rate": 100,
"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": 4,
"rate": 150,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
si.save()
einvoice = make_einvoice(si)
total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']])
total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']])
total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']])
total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']])
total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']])
self.assertEqual(einvoice['Version'], '1.1')
self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value)
self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value)
self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value)
self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value)
self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value)
self.assertTrue(einvoice['EwbDtls'])
def make_sales_invoice_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()
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()
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`
@ -1991,14 +2075,19 @@ def create_sales_invoice(**args):
si.append("items", { si.append("items", {
"item_code": args.item or args.item_code or "_Test Item", "item_code": args.item or args.item_code or "_Test Item",
"item_name": args.item_name or "_Test Item",
"description": args.description or "_Test Item",
"gst_hsn_code": "999800", "gst_hsn_code": "999800",
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 1, "qty": args.qty or 1,
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100, "rate": args.rate if args.get("rate") is not None else 100,
"income_account": args.income_account or "Sales - _TC", "income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no "serial_no": args.serial_no,
"conversion_factor": 1
}) })
if not args.do_not_save: if not args.do_not_save:

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-06-04 11:02:19", "creation": "2013-06-04 11:02:19",
"doctype": "DocType", "doctype": "DocType",
@ -51,6 +52,7 @@
"column_break_24", "column_break_24",
"base_net_rate", "base_net_rate",
"base_net_amount", "base_net_amount",
"incoming_rate",
"drop_ship", "drop_ship",
"delivered_by_supplier", "delivered_by_supplier",
"accounting", "accounting",
@ -792,20 +794,28 @@
"options": "Project" "options": "Project"
}, },
{ {
"depends_on": "eval:parent.update_stock == 1", "depends_on": "eval:parent.update_stock == 1",
"fieldname": "sales_invoice_item", "fieldname": "sales_invoice_item",
"fieldtype": "Data", "fieldtype": "Data",
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"label": "Sales Invoice Item", "label": "Sales Invoice Item",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
} },
{
"fieldname": "incoming_rate",
"fieldtype": "Currency",
"label": "Incoming Rate",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-08-20 11:24:41.749986", "modified": "2020-09-23 19:59:04.879322",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",

View File

@ -5,23 +5,19 @@ 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'): 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:
if not cancel: if not cancel:
validate_accounting_period(gl_map) validate_accounting_period(gl_map)
gl_map = process_gl_map(gl_map, merge_entries) gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1: if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding) save_entries(gl_map, adv_adj, update_outstanding, from_repost)
else: else:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.")) frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else: else:
@ -119,8 +115,9 @@ def check_if_in_list(gle, gl_map, dimensions=None):
if same_head: if same_head:
return e return e
def save_entries(gl_map, adv_adj, update_outstanding): def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
validate_cwip_accounts(gl_map) if not from_repost:
validate_cwip_accounts(gl_map)
round_off_debit_credit(gl_map) round_off_debit_credit(gl_map)
@ -128,76 +125,19 @@ def save_entries(gl_map, adv_adj, update_outstanding):
check_freezing_date(gl_map[0]["posting_date"], adv_adj) check_freezing_date(gl_map[0]["posting_date"], adv_adj)
for entry in gl_map: for entry in gl_map:
make_entry(entry, adv_adj, update_outstanding) make_entry(entry, adv_adj, update_outstanding, from_repost)
# check against budget def make_entry(args, adv_adj, update_outstanding, from_repost=False):
validate_expense_against_budget(entry)
validate_account_for_perpetual_inventory(gl_map)
def make_entry(args, adv_adj, update_outstanding):
gle = frappe.new_doc("GL Entry") gle = frappe.new_doc("GL Entry")
gle.update(args) gle.update(args)
gle.flags.ignore_permissions = 1 gle.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost
gle.insert() gle.insert()
gle.run_method("on_update_with_args", adv_adj, update_outstanding) gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
gle.submit() gle.submit()
# check against budget 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,
getdate(), 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.").format(
stock_bal, account_bal, frappe.bold(account))
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

@ -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):
@ -928,7 +905,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
if expected_gle: if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: else:
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
@ -947,7 +924,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle from `tabStock Ledger Entry` sle
where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition} where
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
and is_cancelled = 0
{condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition), order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True): tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no]) future_stock_vouchers.append([d.voucher_type, d.voucher_no])
@ -964,3 +944,106 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries return gl_entries
def compare_existing_and_expected_gle(existing_gle, expected_gle):
matched = True
for entry in expected_gle:
account_existed = False
for e in existing_gle:
if entry.account == e.account:
account_existed = True
if entry.account == e.account and entry.against_account == e.against_account \
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
and (entry.debit != e.debit or entry.credit != e.credit):
matched = False
break
if not account_existed:
matched = False
break
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

@ -732,7 +732,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-10-30 11:59:47.670951", "modified": "2020-12-07 11:59:47.670951",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

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

@ -16,6 +16,8 @@ from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate
class BuyingController(StockController): class BuyingController(StockController):
def __setup__(self): def __setup__(self):
@ -63,7 +65,7 @@ class BuyingController(StockController):
self.set_landed_cost_voucher_amount() self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"): if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate("items") self.update_valuation_rate()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate) super(BuyingController, self).set_missing_values(for_validate)
@ -177,7 +179,7 @@ class BuyingController(StockController):
self.in_words = money_in_words(amount, self.currency) self.in_words = money_in_words(amount, self.currency)
# update valuation rate # update valuation rate
def update_valuation_rate(self, parentfield): def update_valuation_rate(self, reset_outgoing_rate=True):
""" """
item_tax_amount is the total tax amount applied on that item item_tax_amount is the total tax amount applied on that item
stored for valuation stored for valuation
@ -188,7 +190,7 @@ class BuyingController(StockController):
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1 last_item_idx = 1
for d in self.get(parentfield): for d in self.get("items"):
if d.item_code and d.item_code in stock_and_asset_items: if d.item_code and d.item_code in stock_and_asset_items:
stock_and_asset_items_qty += flt(d.qty) stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount) stock_and_asset_items_amount += flt(d.base_net_amount)
@ -198,7 +200,7 @@ class BuyingController(StockController):
if d.category in ["Valuation", "Valuation and Total"]]) if d.category in ["Valuation", "Valuation and Total"]])
valuation_amount_adjustment = total_valuation_amount valuation_amount_adjustment = total_valuation_amount
for i, item in enumerate(self.get(parentfield)): for i, item in enumerate(self.get("items")):
if item.item_code and item.qty and item.item_code in stock_and_asset_items: if item.item_code and item.qty and item.item_code in stock_and_asset_items:
item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \ item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
else flt(item.qty) / stock_and_asset_items_qty else flt(item.qty) / stock_and_asset_items_qty
@ -216,16 +218,34 @@ class BuyingController(StockController):
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0 item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
qty_in_stock_uom = flt(item.qty * item.conversion_factor) qty_in_stock_uom = flt(item.qty * item.conversion_factor)
rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0 item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost
landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \ + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost
+ landed_cost_voucher_amount) / qty_in_stock_uom)
else: else:
item.valuation_rate = 0.0 item.valuation_rate = 0.0
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0
for d in self.get("supplied_items"):
if d.reference_name == item_row_id:
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
rate = get_incoming_rate({
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * d.consumed_qty,
"serial_no": d.serial_no
})
if rate > 0:
d.rate = rate
d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
supplied_items_cost += flt(d.amount)
return supplied_items_cost
def validate_for_subcontracting(self): def validate_for_subcontracting(self):
if not self.is_subcontracted and self.sub_contracted_items: if not self.is_subcontracted and self.sub_contracted_items:
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No")) frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
@ -352,35 +372,17 @@ class BuyingController(StockController):
else: else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty) self.append_raw_material_to_be_backflushed(item, raw_material, qty)
def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty): def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
rm = self.append('supplied_items', {}) rm = self.append('supplied_items', {})
rm.update(raw_material_data) rm.update(raw_material_data)
if not rm.main_item_code: if not rm.main_item_code:
rm.main_item_code = fg_item_doc.item_code rm.main_item_code = fg_item_row.item_code
rm.reference_name = fg_item_doc.name rm.reference_name = fg_item_row.name
rm.required_qty = qty rm.required_qty = qty
rm.consumed_qty = qty rm.consumed_qty = qty
if not raw_material_data.get('non_stock_item'):
from erpnext.stock.utils import get_incoming_rate
rm.rate = get_incoming_rate({
"item_code": raw_material_data.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * qty,
"serial_no": rm.serial_no
})
if not rm.rate:
rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company)
rm.amount = qty * flt(rm.rate)
fg_item_doc.rm_supp_cost += rm.amount
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1 exploded_item = 1
if hasattr(item, 'include_exploded_items'): if hasattr(item, 'include_exploded_items'):
@ -389,7 +391,7 @@ class BuyingController(StockController):
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item) bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = [] used_alternative_items = []
if self.doctype == 'Purchase Receipt' and item.purchase_order: if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order) used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
raw_materials_cost = 0 raw_materials_cost = 0
@ -406,7 +408,7 @@ class BuyingController(StockController):
reserve_warehouse = None reserve_warehouse = None
conversion_factor = item.conversion_factor conversion_factor = item.conversion_factor
if (self.doctype == 'Purchase Receipt' and item.purchase_order and if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
bom_item.item_code in used_alternative_items): bom_item.item_code in used_alternative_items):
alternative_item_data = used_alternative_items.get(bom_item.item_code) alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code bom_item.item_code = alternative_item_data.item_code
@ -434,9 +436,7 @@ class BuyingController(StockController):
rm.rm_item_code = bom_item.item_code rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty rm.required_qty = required_qty
if self.doctype == "Purchase Order" and not rm.reserve_warehouse: rm.rate = bom_item.rate
rm.reserve_warehouse = reserve_warehouse
rm.conversion_factor = conversion_factor rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@ -444,29 +444,8 @@ class BuyingController(StockController):
rm.description = bom_item.description rm.description = bom_item.description
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no: if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
rm.batch_no = item.batch_no rm.batch_no = item.batch_no
elif not rm.reserve_warehouse:
# get raw materials rate rm.reserve_warehouse = reserve_warehouse
if self.doctype == "Purchase Receipt":
from erpnext.stock.utils import get_incoming_rate
rm.rate = get_incoming_rate({
"item_code": bom_item.item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * required_qty,
"serial_no": rm.serial_no
})
if not rm.rate:
rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company = self.company)
else:
rm.rate = bom_item.rate
rm.amount = required_qty * flt(rm.rate)
raw_materials_cost += flt(rm.amount)
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
item.rm_supp_cost = raw_materials_cost
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table): def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
"""Remove all those child items which are no longer present in main item table""" """Remove all those child items which are no longer present in main item table"""
@ -579,7 +558,8 @@ class BuyingController(StockController):
or (cint(self.is_return) and self.docstatus==2)): or (cint(self.is_return) and self.docstatus==2)):
from_warehouse_sle = self.get_sl_entries(d, { from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty, "actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse "warehouse": d.from_warehouse,
"dependant_sle_voucher_detail_no": d.name
}) })
sl_entries.append(from_warehouse_sle) sl_entries.append(from_warehouse_sle)
@ -589,28 +569,20 @@ class BuyingController(StockController):
"serial_no": cstr(d.serial_no).strip() "serial_no": cstr(d.serial_no).strip()
}) })
if self.is_return: if self.is_return:
filters = { outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
"voucher_type": self.doctype,
"voucher_no": self.return_against,
"item_code": d.item_code
}
if (self.doctype == "Purchase Invoice" and self.update_stock
and d.get("purchase_invoice_item")):
filters["voucher_detail_no"] = d.purchase_invoice_item
elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
filters["voucher_detail_no"] = d.purchase_receipt_item
original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
sle.update({ sle.update({
"outgoing_rate": original_incoming_rate "outgoing_rate": outgoing_rate,
"recalculate_rate": 1
}) })
if d.from_warehouse:
sle.dependant_sle_voucher_detail_no = d.name
else: else:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9 val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
incoming_rate = flt(d.valuation_rate, val_rate_db_precision) incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
sle.update({ sle.update({
"incoming_rate": incoming_rate "incoming_rate": incoming_rate,
"recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0
}) })
sl_entries.append(sle) sl_entries.append(sle)
@ -618,7 +590,8 @@ class BuyingController(StockController):
or (cint(self.is_return) and self.docstatus==1)): or (cint(self.is_return) and self.docstatus==1)):
from_warehouse_sle = self.get_sl_entries(d, { from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty, "actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse "warehouse": d.from_warehouse,
"recalculate_rate": 1
}) })
sl_entries.append(from_warehouse_sle) sl_entries.append(from_warehouse_sle)
@ -666,6 +639,7 @@ class BuyingController(StockController):
"item_code": d.rm_item_code, "item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse, "warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty), "actual_qty": -1*flt(d.consumed_qty),
"dependant_sle_voucher_detail_no": d.reference_name
})) }))
def on_submit(self): def on_submit(self):
@ -857,6 +831,7 @@ class BuyingController(StockController):
else: else:
validate_item_type(self, "is_purchase_item", "purchase") validate_item_type(self, "is_purchase_item", "purchase")
def get_items_from_bom(item_code, bom, exploded_item=1): def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item" doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"

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,10 +316,12 @@ 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
@ -330,6 +343,10 @@ 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
@ -365,3 +382,63 @@ def make_return_doc(doctype, source_name, target_doc=None):
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doclist return doclist
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, item_row=None, voucher_detail_no=None):
if not return_against:
return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against")
return_against_item_field = get_return_against_item_fields(voucher_type)
filters = get_filters(voucher_type, voucher_no, voucher_detail_no,
return_against, item_code, return_against_item_field, item_row)
if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
select_field = "incoming_rate"
else:
select_field = "abs(stock_value_difference / actual_qty)"
return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
def get_return_against_item_fields(voucher_type):
return_against_item_fields = {
"Purchase Receipt": "purchase_receipt_item",
"Purchase Invoice": "purchase_invoice_item",
"Delivery Note": "dn_detail",
"Sales Invoice": "sales_invoice_item"
}
return return_against_item_fields[voucher_type]
def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, item_code, return_against_item_field, item_row):
filters = {
"voucher_type": voucher_type,
"voucher_no": return_against,
"item_code": item_code
}
if item_row:
reference_voucher_detail_no = item_row.get(return_against_item_field)
else:
reference_voucher_detail_no = frappe.db.get_value(voucher_type + " Item", voucher_detail_no, return_against_item_field)
if reference_voucher_detail_no:
filters["voucher_detail_no"] = reference_voucher_detail_no
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

@ -13,6 +13,7 @@ from frappe.contacts.doctype.address.address import get_address_display
from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController): class SellingController(StockController):
def __setup__(self): def __setup__(self):
@ -48,6 +49,7 @@ class SellingController(StockController):
self.set_customer_address() self.set_customer_address()
self.validate_for_duplicate_items() self.validate_for_duplicate_items()
self.validate_target_warehouse() self.validate_target_warehouse()
self.set_incoming_rate()
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
@ -230,7 +232,8 @@ class SellingController(StockController):
'voucher_type': self.doctype, 'voucher_type': self.doctype,
'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"),
'delivery_note_item': d.get("dn_detail") 'dn_detail': d.get("dn_detail"),
'incoming_rate': p.incoming_rate
})) }))
else: else:
il.append(frappe._dict({ il.append(frappe._dict({
@ -248,7 +251,8 @@ class SellingController(StockController):
'voucher_type': self.doctype, 'voucher_type': self.doctype,
'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"),
'delivery_note_item': d.get("dn_detail") 'dn_detail': d.get("dn_detail"),
'incoming_rate': d.incoming_rate
})) }))
return il return il
@ -307,69 +311,89 @@ class SellingController(StockController):
sales_order.update_reserved_qty(so_item_rows) sales_order.update_reserved_qty(so_item_rows)
def set_incoming_rate(self):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not cint(self.get("is_return")):
# Get incoming rate based on original item cost based on valuation method
d.incoming_rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1*flt(d.qty),
"serial_no": d.serial_no,
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation")
}, raise_error_if_no_rate=False)
elif self.get("return_against"):
# Get incoming rate of return entry from reference document
# based on original item cost as per valuation method
d.incoming_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
def update_stock_ledger(self): def update_stock_ledger(self):
self.update_reserved_qty() self.update_reserved_qty()
sl_entries = [] sl_entries = []
# Loop over items and packed items table
for d in self.get_item_list(): for d in self.get_item_list():
if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty): if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty):
if flt(d.conversion_factor)==0.0: if flt(d.conversion_factor)==0.0:
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0 d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
return_rate = 0
if cint(self.is_return) and self.return_against and self.docstatus==1:
against_document_no = (d.get("sales_invoice_item")
if self.doctype == "Sales Invoice" else d.get("delivery_note_item"))
return_rate = self.get_incoming_rate_for_return(d.item_code, # On cancellation or return entry submission, make stock ledger entry for
self.return_against, against_document_no)
# On cancellation or if return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly # target warehouse first, to update serial no values properly
if d.warehouse and ((not cint(self.is_return) and self.docstatus==1) if d.warehouse and ((not cint(self.is_return) and self.docstatus==1)
or (cint(self.is_return) and self.docstatus==2)): or (cint(self.is_return) and self.docstatus==2)):
sl_entries.append(self.get_sl_entries(d, { sl_entries.append(self.get_sle_for_source_warehouse(d))
"actual_qty": -1*flt(d.qty),
"incoming_rate": return_rate
}))
if d.target_warehouse: if d.target_warehouse:
target_warehouse_sle = self.get_sl_entries(d, { sl_entries.append(self.get_sle_for_target_warehouse(d))
"actual_qty": flt(d.qty),
"warehouse": d.target_warehouse
})
if self.docstatus == 1:
if not cint(self.is_return):
args = frappe._dict({
"item_code": d.item_code,
"warehouse": d.warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1*flt(d.qty),
"serial_no": d.serial_no,
"company": d.company,
"voucher_type": d.voucher_type,
"voucher_no": d.name,
"allow_zero_valuation": d.allow_zero_valuation
})
target_warehouse_sle.update({
"incoming_rate": get_incoming_rate(args)
})
else:
target_warehouse_sle.update({
"outgoing_rate": return_rate
})
sl_entries.append(target_warehouse_sle)
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2) if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
or (cint(self.is_return) and self.docstatus==1)): or (cint(self.is_return) and self.docstatus==1)):
sl_entries.append(self.get_sl_entries(d, { sl_entries.append(self.get_sle_for_source_warehouse(d))
"actual_qty": -1*flt(d.qty),
"incoming_rate": return_rate
}))
self.make_sl_entries(sl_entries) self.make_sl_entries(sl_entries)
def get_sle_for_source_warehouse(self, item_row):
sle = self.get_sl_entries(item_row, {
"actual_qty": -1*flt(item_row.qty),
"incoming_rate": item_row.incoming_rate,
"recalculate_rate": cint(self.is_return)
})
if item_row.target_warehouse and not cint(self.is_return):
sle.dependant_sle_voucher_detail_no = item_row.name
return sle
def get_sle_for_target_warehouse(self, item_row):
sle = self.get_sl_entries(item_row, {
"actual_qty": flt(item_row.qty),
"warehouse": item_row.target_warehouse
})
if self.docstatus == 1:
if not cint(self.is_return):
sle.update({
"incoming_rate": item_row.incoming_rate,
"recalculate_rate": 1
})
else:
sle.update({
"outgoing_rate": item_row.incoming_rate
})
if item_row.warehouse:
sle.dependant_sle_voucher_detail_no = item_row.name
return sle
def set_po_nos(self, for_validate=False): def set_po_nos(self, for_validate=False):
if self.doctype == 'Sales Invoice' and hasattr(self, "items"): if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
if for_validate and self.po_no: if for_validate and self.po_no:

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
@ -24,7 +24,7 @@ class StockController(AccountsController):
self.validate_serialized_batch() self.validate_serialized_batch()
self.validate_customer_provided_item() self.validate_customer_provided_item()
def make_gl_entries(self, gl_entries=None): def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2: if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@ -34,12 +34,12 @@ class StockController(AccountsController):
if self.docstatus==1: if self.docstatus==1:
if not gl_entries: if not gl_entries:
gl_entries = self.get_gl_entries(warehouse_account) gl_entries = self.get_gl_entries(warehouse_account)
make_gl_entries(gl_entries) make_gl_entries(gl_entries, from_repost=from_repost)
elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1: elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1:
gl_entries = [] gl_entries = []
gl_entries = self.get_asset_gl_entry(gl_entries) gl_entries = self.get_asset_gl_entry(gl_entries)
make_gl_entries(gl_entries) make_gl_entries(gl_entries, from_repost=from_repost)
def validate_serialized_batch(self): def validate_serialized_batch(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@ -70,7 +70,6 @@ class StockController(AccountsController):
gl_list = [] gl_list = []
warehouse_with_no_account = [] warehouse_with_no_account = []
precision = frappe.get_precision("GL Entry", "debit_in_account_currency") precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
for item_row in voucher_details: for item_row in voucher_details:
sle_list = sle_map.get(item_row.name) sle_list = sle_map.get(item_row.name)
@ -125,7 +124,7 @@ class StockController(AccountsController):
if warehouse_with_no_account: if warehouse_with_no_account:
for wh in warehouse_with_no_account: for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"): if frappe.db.get_value("Warehouse", wh, "company"):
frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company)) frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
return process_gl_map(gl_list) return process_gl_map(gl_list)
@ -309,23 +308,6 @@ class StockController(AccountsController):
return serialized_items return serialized_items
def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None):
incoming_rate = 0.0
cond = ''
if against_document and item_code:
if against_document_no:
cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no))
incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
from `tabStock Ledger Entry`
where voucher_type = %s and voucher_no = %s
and item_code = %s {0} limit 1""".format(cond),
(self.doctype, against_document, item_code))
incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
return incoming_rate
def validate_warehouse(self): def validate_warehouse(self):
from erpnext.stock.utils import validate_warehouse_company from erpnext.stock.utils import validate_warehouse_company
@ -409,19 +391,72 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1 d.allow_zero_valuation_rate = 1
def compare_existing_and_expected_gle(existing_gle, expected_gle): def repost_future_sle_and_gle(self):
matched = True args = frappe._dict({
for entry in expected_gle: "posting_date": self.posting_date,
account_existed = False "posting_time": self.posting_time,
for e in existing_gle: "voucher_type": self.doctype,
if entry.account == e.account: "voucher_no": self.name,
account_existed = True "company": self.company
if entry.account == e.account and entry.against_account == e.against_account \ })
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
and (entry.debit != e.debit or entry.credit != e.credit): if check_if_future_sle_exists(args):
matched = False create_repost_item_valuation_entry(args)
break elif not is_reposting_pending():
if not account_existed: check_if_stock_and_account_balance_synced(self.posting_date,
matched = False 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):
sl_entries = frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"],
order_by="creation asc")
distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries]))
sle_exists = False
for item_code, warehouse in distinct_item_warehouses:
args.update({
"item_code": item_code,
"warehouse": warehouse
})
if get_sle(args):
sle_exists = True
break break
return matched return sle_exists
def get_sle(args):
return frappe.db.sql("""
select name
from `tabStock Ledger Entry`
where
item_code=%(item_code)s
and warehouse=%(warehouse)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
limit 1
""", args)
def create_repost_item_valuation_entry(args):
args = frappe._dict(args)
repost_entry = frappe.new_doc("Repost Item Valuation")
repost_entry.based_on = args.based_on
if not args.based_on:
repost_entry.based_on = 'Transaction' if args.voucher_no else "Item and Warehouse"
repost_entry.voucher_type = args.voucher_type
repost_entry.voucher_no = args.voucher_no
repost_entry.item_code = args.item_code
repost_entry.warehouse = args.warehouse
repost_entry.posting_date = args.posting_date
repost_entry.posting_time = args.posting_time
repost_entry.company = args.company
repost_entry.allow_zero_rate = args.allow_zero_rate
repost_entry.flags.ignore_links = True
repost_entry.save()
repost_entry.submit()

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

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

@ -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,7 +111,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-15 15:18:15.227848", "modified": "2020-12-17 16:27:20.311060",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Policy Assignment", "name": "Leave Policy Assignment",
@ -127,6 +127,7 @@
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
@ -139,6 +140,7 @@
"report": 1, "report": 1,
"role": "HR User", "role": "HR User",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
}, },
{ {
@ -151,6 +153,7 @@
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],

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,8 +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.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
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
@ -15,10 +14,10 @@ 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):
set_perpetual_inventory(0)
self.warehouse = '_Test Warehouse 2 - _TC' self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item' self.item = '_Test Item'
@ -371,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, {
@ -511,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

@ -732,6 +732,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

View File

@ -0,0 +1,55 @@
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("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,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

@ -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_net_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,772 @@
# -*- 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
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):
address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
gstin = address.get('gstin')
gstin_details = get_gstin_details(gstin)
legal_name = gstin_details.get('LegalName')
location = gstin_details.get('AddrLoc') or address.get('city')
state_code = gstin_details.get('StateCode')
pincode = gstin_details.get('AddrPncd')
address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno'))
address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt'))
email_id = address.get('email_id')
phone = address.get('phone')
# get last 10 digit
phone = phone.replace(" ", "")[-10:] if phone else ''
if state_code == 97:
# according to einvoice standard
pincode = 999999
return frappe._dict(dict(
gstin=gstin, legal_name=legal_name, location=location,
pincode=pincode, state_code=state_code, address_line1=address_line1,
address_line2=address_line2, email=email_id, phone=phone
))
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, phone, email_id = frappe.db.get_value(
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id']
)
return frappe._dict(dict(
gstin='URP', legal_name=address_title, address_line1=address_line1,
address_line2=address_line2, email=email_id, phone=phone,
pincode=999999, state_code=96, place_of_supply=96, location=city
))
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.qty = abs(item.qty)
item.description = d.item_name
item.taxable_value = abs(item.base_net_amount)
item.discount_amount = abs(item.discount_amount * item.qty)
item.unit_rate = abs(item.base_price_list_rate) if item.discount_amount else abs(item.base_net_rate)
item.gross_amount = abs(item.unit_rate * item.qty)
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 = 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:
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
if t.charge_type == 'On Item Quantity':
item.cess_nadv_amount += abs(item_tax_detail[1])
else:
item.cess_rate += item_tax_detail[0]
item.cess_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.igst_account:
item.tax_rate += item_tax_detail[0]
item.igst_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.sgst_account:
item.tax_rate += item_tax_detail[0]
item.sgst_amount += abs(item_tax_detail[1])
elif t.account_head in gst_accounts.cgst_account:
item.tax_rate += item_tax_detail[0]
item.cgst_amount += abs(item_tax_detail[1])
return item
def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
invoice_value_details.base_net_total = abs(invoice.base_net_total)
invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0
# discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off
invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0)
disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total')
invoice_value_details.base_grand_total = abs(invoice.base_grand_total) if disable_rounded else abs(invoice.base_rounded_total)
invoice_value_details.grand_total = abs(invoice.grand_total) if disable_rounded else abs(invoice.rounded_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:
invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.igst_account:
invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.sgst_account:
invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount)
elif t.account_head in gst_accounts.cgst_account:
invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount)
else:
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_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 make_einvoice(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'))
einvoice[fieldname] = flt(value, 2) 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 + 'test/enriched/ei/api/master/gstin'
self.generate_irn_url = self.base_url + 'test/enriched/ei/api/invoice'
self.irn_details_url = self.base_url + 'test/enriched/ei/api/invoice/irn'
self.cancel_irn_url = self.base_url + 'test/enriched/ei/api/invoice/cancel'
self.cancel_ewaybill_url = self.base_url + '/test/enriched/ei/api/ewayapi'
self.generate_ewaybill_url = self.base_url + 'test/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

@ -785,7 +785,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-05-29 20:54:32.309460", "modified": "2020-012-07 20:54:32.309460",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

@ -7,7 +7,8 @@
"doctype": "Company", "doctype": "Company",
"domain": "Manufacturing", "domain": "Manufacturing",
"chart_of_accounts": "Standard", "chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List" "default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
}, },
{ {
"abbr": "_TC1", "abbr": "_TC1",
@ -17,7 +18,8 @@
"doctype": "Company", "doctype": "Company",
"domain": "Retail", "domain": "Retail",
"chart_of_accounts": "Standard", "chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List" "default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
}, },
{ {
"abbr": "_TC2", "abbr": "_TC2",
@ -27,7 +29,8 @@
"doctype": "Company", "doctype": "Company",
"domain": "Retail", "domain": "Retail",
"chart_of_accounts": "Standard", "chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List" "default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
}, },
{ {
"abbr": "_TC3", "abbr": "_TC3",
@ -38,7 +41,8 @@
"doctype": "Company", "doctype": "Company",
"domain": "Manufacturing", "domain": "Manufacturing",
"chart_of_accounts": "Standard", "chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List" "default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
}, },
{ {
"abbr": "_TC4", "abbr": "_TC4",
@ -50,7 +54,8 @@
"doctype": "Company", "doctype": "Company",
"domain": "Manufacturing", "domain": "Manufacturing",
"chart_of_accounts": "Standard", "chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List" "default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
}, },
{ {
"abbr": "_TC5", "abbr": "_TC5",
@ -61,7 +66,8 @@
"doctype": "Company", "doctype": "Company",
"domain": "Manufacturing", "domain": "Manufacturing",
"chart_of_accounts": "Standard", "chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List" "default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
}, },
{ {
"abbr": "TCP1", "abbr": "TCP1",

View File

@ -8,13 +8,8 @@ import unittest
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
from frappe.utils import cint, flt from frappe.utils import cint, flt
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestBatch(unittest.TestCase): class TestBatch(unittest.TestCase):
def setUp(self):
set_perpetual_inventory(0)
def test_item_has_batch_enabled(self): def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({ self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch", "doctype": "Batch",

View File

@ -16,22 +16,30 @@ class Bin(Document):
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin''' '''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args) self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation": if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"): if not args.get("posting_date"):
args["posting_date"] = nowdate() args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({ update_entries_after({
"item_code": self.item_code, "item_code": self.item_code,
"warehouse": self.warehouse, "warehouse": self.warehouse,
"posting_date": args.get("posting_date"), "posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"), "posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"), "voucher_no": args.get("voucher_no"),
"sle_id": args.sle_id "sle_id": args.name
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# Update qty_after_transaction in future SLEs of this item and warehouse
update_qty_in_future_sle(args)
def update_qty(self, args): def update_qty(self, args):
# update the stock values (for current quantities) # update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation": if args.get("voucher_type")=="Stock Reconciliation":

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

@ -217,6 +217,7 @@ class DeliveryNote(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO # because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle()
def on_cancel(self): def on_cancel(self):
super(DeliveryNote, self).on_cancel() super(DeliveryNote, self).on_cancel()
@ -234,7 +235,8 @@ class DeliveryNote(SellingController):
self.cancel_packing_slips() self.cancel_packing_slips()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def check_credit_limit(self): def check_credit_limit(self):
from erpnext.selling.doctype.customer.customer import check_credit_limit from erpnext.selling.doctype.customer.customer import check_credit_limit
@ -596,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:
@ -607,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",
@ -615,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

@ -10,8 +10,7 @@ import frappe.defaults
from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on from erpnext.accounts.utils import get_balance_on
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
import get_gl_entries, set_perpetual_inventory
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice, make_delivery_trip from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice, make_delivery_trip
from erpnext.stock.doctype.stock_entry.test_stock_entry \ from erpnext.stock.doctype.stock_entry.test_stock_entry \
import make_stock_entry, make_serialized_item, get_qty_after_transaction import make_stock_entry, make_serialized_item, get_qty_after_transaction
@ -24,9 +23,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
class TestDeliveryNote(unittest.TestCase): class TestDeliveryNote(unittest.TestCase):
def setUp(self):
set_perpetual_inventory(0)
def test_over_billing_against_dn(self): def test_over_billing_against_dn(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@ -43,7 +39,6 @@ class TestDeliveryNote(unittest.TestCase):
def test_delivery_note_no_gl_entry(self): def test_delivery_note_no_gl_entry(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
set_perpetual_inventory(0, company)
make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100)
stock_queue = json.loads(get_previous_sle({ stock_queue = json.loads(get_previous_sle({

View File

@ -56,6 +56,7 @@
"base_net_rate", "base_net_rate",
"base_net_amount", "base_net_amount",
"billed_amt", "billed_amt",
"incoming_rate",
"item_weight_details", "item_weight_details",
"weight_per_unit", "weight_per_unit",
"total_weight", "total_weight",
@ -732,16 +733,22 @@
"depends_on": "returned_qty", "depends_on": "returned_qty",
"fieldname": "returned_qty", "fieldname": "returned_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Returned Qty in Stock UOM", "label": "Returned Qty in Stock UOM"
},
{
"fieldname": "incoming_rate",
"fieldtype": "Currency",
"label": "Incoming Rate",
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-07-31 20:12:43.054342", "modified": "2020-12-07 19:59:27.119856",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@ -458,5 +458,15 @@
"item_tax_template": "_Test Item Tax Template 1" "item_tax_template": "_Test Item Tax Template 1"
} }
] ]
},
{
"description": "_Test",
"doctype": "Item",
"is_stock_item": 1,
"item_code": "138-CMS Shoe",
"item_group": "_Test Item Group",
"item_name": "138-CMS Shoe",
"stock_uom": "_Test UOM",
"gst_hsn_code": "999800"
} }
] ]

View File

@ -12,11 +12,9 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry
import unittest import unittest
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestItemAlternative(unittest.TestCase): class TestItemAlternative(unittest.TestCase):
def setUp(self): def setUp(self):
set_perpetual_inventory(0)
make_items() make_items()
def test_alternative_item_for_subcontract_rm(self): def test_alternative_item_for_subcontract_rm(self):

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2014-07-11 11:51:00.453717", "creation": "2014-07-11 11:51:00.453717",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -31,16 +32,19 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"fieldname": "expense_account", "fieldname": "expense_account",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Expense Account", "label": "Expense Account",
"mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"options": "Account", "options": "Account",
"reqd": 1 "print_hide": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-09-30 18:28:32.070655", "links": [],
"modified": "2020-12-04 00:22:14.373312",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Landed Cost Taxes and Charges", "name": "Landed Cost Taxes and Charges",

View File

@ -77,9 +77,9 @@ class LandedCostVoucher(Document):
company_currency = erpnext.get_company_currency(self.company) company_currency = erpnext.get_company_currency(self.company)
for account in self.taxes: for account in self.taxes:
if get_account_currency(account.expense_account) != company_currency: if get_account_currency(account.expense_account) != company_currency:
frappe.throw(msg=_(""" Row {0}: Expense account currency should be same as company's default currency. frappe.throw(_("Row {}: Expense account currency should be same as company's default currency.").format(account.idx)
Please select expense account with account currency as {1}""") + _("Please select expense account with account currency as {}.").format(frappe.bold(company_currency)),
.format(account.idx, frappe.bold(company_currency)), title=_("Invalid Account Currency")) title=_("Invalid Account Currency"))
def set_total_taxes_and_charges(self): def set_total_taxes_and_charges(self):
self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")]) self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")])
@ -121,7 +121,7 @@ class LandedCostVoucher(Document):
doc.set_landed_cost_voucher_amount() doc.set_landed_cost_voucher_amount()
# set valuation amount in pr item # set valuation amount in pr item
doc.update_valuation_rate("items") doc.update_valuation_rate(reset_outgoing_rate=False)
# db_update will update and save landed_cost_voucher_amount and voucher_amount in PR # db_update will update and save landed_cost_voucher_amount and voucher_amount in PR
for item in doc.get("items"): for item in doc.get("items"):
@ -143,6 +143,7 @@ class LandedCostVoucher(Document):
doc.docstatus = 1 doc.docstatus = 1
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
doc.make_gl_entries() doc.make_gl_entries()
doc.repost_future_sle_and_gle()
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
for item in self.get('items'): for item in self.get('items'):
@ -152,14 +153,13 @@ class LandedCostVoucher(Document):
docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document,
'item_code': item.item_code }, fields=['name', 'docstatus']) 'item_code': item.item_code }, fields=['name', 'docstatus'])
if not docs or len(docs) != item.qty: if not docs or len(docs) != item.qty:
frappe.throw(_('There are not enough asset created or linked to {0}. \ frappe.throw(_('There are not enough asset created or linked to {0}.').format(item.receipt_document)
Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty)) + _('Please create or link {0} Assets with respective document.').format(item.qty))
if docs: if docs:
for d in docs: for d in docs:
if d.docstatus == 1: if d.docstatus == 1:
frappe.throw(_('{2} <b>{0}</b> has submitted Assets.\ frappe.throw(_('{0} {1} has submitted Assets. Remove Item {2} from table to continue.')
Remove Item <b>{1}</b> from table to continue.').format( .format(item.receipt_document_type, frappe.bold(item.receipt_document), frappe.bold(item.item_code)))
item.receipt_document, item.item_code, item.receipt_document_type))
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
for item in receipt_document.get("items"): for item in receipt_document.get("items"):

View File

@ -7,7 +7,7 @@ import unittest
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
import set_perpetual_inventory, get_gl_entries, test_records as pr_test_records, make_purchase_receipt import get_gl_entries, test_records as pr_test_records, make_purchase_receipt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
@ -27,7 +27,7 @@ class TestLandedCostVoucher(unittest.TestCase):
}, },
fieldname=["qty_after_transaction", "stock_value"], as_dict=1) fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount") pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount")
self.assertEqual(pr_lc_value, 25.0) self.assertEqual(pr_lc_value, 25.0)
@ -89,7 +89,7 @@ class TestLandedCostVoucher(unittest.TestCase):
}, },
fieldname=["qty_after_transaction", "stock_value"], as_dict=1) fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
submit_landed_cost_voucher("Purchase Invoice", pi.name, pi.company) create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name}, pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name},
"landed_cost_voucher_amount") "landed_cost_voucher_amount")
@ -137,7 +137,7 @@ class TestLandedCostVoucher(unittest.TestCase):
serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate") serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate")
submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
serial_no = frappe.db.get_value("Serial No", "SN001", serial_no = frappe.db.get_value("Serial No", "SN001",
["warehouse", "purchase_rate"], as_dict=1) ["warehouse", "purchase_rate"], as_dict=1)
@ -160,7 +160,7 @@ class TestLandedCostVoucher(unittest.TestCase):
}) })
pr.submit() pr.submit()
lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22) lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
self.assertEqual(lcv.items[0].applicable_charges, 41.07) self.assertEqual(lcv.items[0].applicable_charges, 41.07)
self.assertEqual(lcv.items[2].applicable_charges, 41.08) self.assertEqual(lcv.items[2].applicable_charges, 41.08)
@ -236,7 +236,7 @@ def make_landed_cost_voucher(** args):
return lcv return lcv
def submit_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50): def create_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50):
ref_doc = frappe.get_doc(receipt_document_type, receipt_document) ref_doc = frappe.get_doc(receipt_document_type, receipt_document)
lcv = frappe.new_doc("Landed Cost Voucher") lcv = frappe.new_doc("Landed Cost Voucher")

View File

@ -12,9 +12,6 @@ from erpnext.stock.doctype.material_request.material_request \
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase): class TestMaterialRequest(unittest.TestCase):
def setUp(self):
erpnext.set_perpetual_inventory(0)
def test_make_purchase_order(self): def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert() mr = frappe.copy_doc(test_records[0]).insert()

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2013-02-22 01:28:00", "creation": "2013-02-22 01:28:00",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -14,6 +15,7 @@
"target_warehouse", "target_warehouse",
"column_break_9", "column_break_9",
"qty", "qty",
"uom",
"section_break_9", "section_break_9",
"serial_no", "serial_no",
"column_break_11", "column_break_11",
@ -23,7 +25,7 @@
"actual_qty", "actual_qty",
"projected_qty", "projected_qty",
"column_break_16", "column_break_16",
"uom", "incoming_rate",
"page_break", "page_break",
"prevdoc_doctype", "prevdoc_doctype",
"parent_detail_docname" "parent_detail_docname"
@ -199,11 +201,21 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "incoming_rate",
"fieldtype": "Currency",
"label": "Incoming Rate",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"modified": "2019-11-26 20:09:59.400960", "links": [],
"modified": "2020-09-24 09:25:13.050151",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Packed Item", "name": "Packed Item",

View File

@ -181,6 +181,7 @@ class PurchaseReceipt(BuyingController):
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle()
def check_next_docstatus(self): def check_next_docstatus(self):
submit_rv = frappe.db.sql("""select t1.name submit_rv = frappe.db.sql("""select t1.name
@ -209,7 +210,8 @@ class PurchaseReceipt(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO # because updating ordered qty in bin depends upon updated ordered qty in PO
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.repost_future_sle_and_gle()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches() self.delete_auto_created_batches()
def get_current_stock(self): def get_current_stock(self):
@ -323,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

@ -9,14 +9,15 @@ import frappe.defaults
from frappe.utils import cint, flt, cstr, today, random_string, add_days from frappe.utils import cint, flt, cstr, today, random_string, add_days
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError
from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from six import iteritems from six import iteritems
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestPurchaseReceipt(unittest.TestCase): class TestPurchaseReceipt(unittest.TestCase):
def setUp(self): def setUp(self):
set_perpetual_inventory(0)
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_reverse_purchase_receipt_sle(self): def test_reverse_purchase_receipt_sle(self):
@ -112,6 +113,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertFalse(get_gl_entries("Purchase Receipt", pr.name)) self.assertFalse(get_gl_entries("Purchase Receipt", pr.name))
pr.cancel()
def test_batched_serial_no_purchase(self): def test_batched_serial_no_purchase(self):
item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'}) item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'})
if not item: if not item:
@ -184,21 +187,29 @@ class TestPurchaseReceipt(unittest.TestCase):
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")]) rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2)) self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
pr.cancel()
def test_subcontracting_gle_fg_item_rate_zero(self): def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
set_perpetual_inventory()
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM") frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1", se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1",
qty=100, basic_rate=100, company="_Test Company with perpetual inventory") qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
se2 = make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes", pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1') company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
supplier_warehouse='Work In Progress - TCP1')
gl_entries = get_gl_entries("Purchase Receipt", pr.name) gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries) self.assertFalse(gl_entries)
set_perpetual_inventory(0) pr.cancel()
se1.cancel()
se2.cancel()
def test_subcontracting_over_receipt(self): def test_subcontracting_over_receipt(self):
""" """
@ -216,13 +227,13 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code=item_code) make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1, po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
#stock raw materials in a warehouse before transfer #stock raw materials in a warehouse before transfer
make_stock_entry(target="_Test Warehouse - _TC", se1 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=1, basic_rate=100) item_code = "Test Extra Item 1", qty=10, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC", se2 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test FG Item", qty=1, basic_rate=100) item_code = "_Test FG Item", qty=1, basic_rate=100)
rm_items = [ rm_items = [
{ {
@ -254,6 +265,13 @@ class TestPurchaseReceipt(unittest.TestCase):
pr1.submit() pr1.submit()
self.assertRaises(frappe.ValidationError, pr2.submit) self.assertRaises(frappe.ValidationError, pr2.submit)
pr1.cancel()
se.cancel()
se1.cancel()
se2.cancel()
po.reload()
po.cancel()
def test_serial_no_supplier(self): def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1) pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"), self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
@ -284,6 +302,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"), self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_warehouse) pr.get("items")[0].rejected_warehouse)
pr.cancel()
def test_purchase_return_partial(self): def test_purchase_return_partial(self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
@ -371,6 +391,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr.per_returned, 100) self.assertEqual(pr.per_returned, 100)
self.assertEqual(pr.status, 'Return Issued') self.assertEqual(pr.status, 'Return Issued')
return_pr.cancel()
pr.cancel()
def test_purchase_return_for_rejected_qty(self): def test_purchase_return_for_rejected_qty(self):
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
@ -388,6 +411,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(actual_qty, -2) self.assertEqual(actual_qty, -2)
return_pr.cancel()
pr.cancel()
def test_purchase_return_for_serialized_items(self): def test_purchase_return_for_serialized_items(self):
def _check_serial_no_values(serial_no, field_values): def _check_serial_no_values(serial_no, field_values):
@ -415,6 +441,10 @@ class TestPurchaseReceipt(unittest.TestCase):
"delivery_document_no": return_pr.name "delivery_document_no": return_pr.name
}) })
return_pr.cancel()
pr.reload()
pr.cancel()
def test_purchase_return_for_multi_uom(self): def test_purchase_return_for_multi_uom(self):
item_code = "_Test Purchase Return For Multi-UOM" item_code = "_Test Purchase Return For Multi-UOM"
if not frappe.db.exists('Item', item_code): if not frappe.db.exists('Item', item_code):
@ -431,6 +461,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0) self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0)
return_pr.cancel()
pr.cancel()
def test_closed_purchase_receipt(self): def test_closed_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_purchase_receipt_status from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_purchase_receipt_status
@ -440,6 +473,9 @@ class TestPurchaseReceipt(unittest.TestCase):
update_purchase_receipt_status(pr.name, "Closed") update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed") self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
pr.reload()
pr.cancel()
def test_pr_billing_status(self): def test_pr_billing_status(self):
# PO -> PR1 -> PI and PO -> PI and PO -> PR2 # PO -> PR1 -> PI and PO -> PI and PO -> PR2
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@ -482,6 +518,16 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(pr2.per_billed, 80) self.assertEqual(pr2.per_billed, 80)
self.assertEqual(pr2.status, "To Bill") self.assertEqual(pr2.status, "To Bill")
pr2.cancel()
pi2.reload()
pi2.cancel()
pi1.reload()
pi1.cancel()
pr1.reload()
pr1.cancel()
po.reload()
po.cancel()
def test_serial_no_against_purchase_receipt(self): def test_serial_no_against_purchase_receipt(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@ -509,6 +555,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(serial_no, frappe.db.get_value("Serial No", self.assertEqual(serial_no, frappe.db.get_value("Serial No",
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name")) {"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name"))
new_pr_doc.cancel()
def test_not_accept_duplicate_serial_no(self): def test_not_accept_duplicate_serial_no(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@ -519,15 +567,18 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = item.name item_code = item.name
serial_no = random_string(5) serial_no = random_string(5)
make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no) pr1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no) dn = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no)
pr = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True) pr2 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
self.assertRaises(SerialNoDuplicateError, pr.submit) self.assertRaises(SerialNoDuplicateError, pr2.submit)
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()
pr1.cancel()
def test_auto_asset_creation(self): def test_auto_asset_creation(self):
asset_item = "Test Asset Item" asset_item = "Test Asset Item"
@ -549,7 +600,7 @@ class TestPurchaseReceipt(unittest.TestCase):
'company_name': '_Test Company', 'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC', 'fixed_asset_account': '_Test Fixed Asset - _TC',
'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC', 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
'depreciation_expense_account': '_Test Depreciation - _TC' 'depreciation_expense_account': '_Test Depreciations - _TC'
}] }]
}).insert() }).insert()
@ -568,6 +619,8 @@ class TestPurchaseReceipt(unittest.TestCase):
location = frappe.db.get_value('Asset', assets[0].name, 'location') location = frappe.db.get_value('Asset', assets[0].name, 'location')
self.assertEquals(location, "Test Location") self.assertEquals(location, "Test Location")
pr.cancel()
def test_purchase_return_with_submitted_asset(self): def test_purchase_return_with_submitted_asset(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
@ -594,6 +647,9 @@ class TestPurchaseReceipt(unittest.TestCase):
pr_return.submit() pr_return.submit()
pr_return.cancel()
pr.cancel()
def test_purchase_receipt_cost_center(self): def test_purchase_receipt_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - TCP1" cost_center = "_Test Cost Center for BS Account - TCP1"
@ -605,7 +661,8 @@ class TestPurchaseReceipt(unittest.TestCase):
'location_name': 'Test Location' 'location_name': 'Test Location'
}).insert() }).insert()
pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1") pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse) stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
gl_entries = get_gl_entries("Purchase Receipt", pr.name) gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@ -623,6 +680,8 @@ class TestPurchaseReceipt(unittest.TestCase):
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
pr.cancel()
def test_purchase_receipt_cost_center_with_balance_sheet_account(self): def test_purchase_receipt_cost_center_with_balance_sheet_account(self):
if not frappe.db.exists('Location', 'Test Location'): if not frappe.db.exists('Location', 'Test Location'):
frappe.get_doc({ frappe.get_doc({
@ -648,6 +707,8 @@ class TestPurchaseReceipt(unittest.TestCase):
for i, gle in enumerate(gl_entries): for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
pr.cancel()
def test_make_purchase_invoice_from_pr_for_returned_qty(self): def test_make_purchase_invoice_from_pr_for_returned_qty(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po
@ -663,6 +724,12 @@ class TestPurchaseReceipt(unittest.TestCase):
pi = make_purchase_invoice(pr.name) pi = make_purchase_invoice(pr.name)
self.assertEquals(pi.items[0].qty, 3) self.assertEquals(pi.items[0].qty, 3)
pr1.cancel()
pr.reload()
pr.cancel()
po.reload()
po.cancel()
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self): def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self):
pr1 = make_purchase_receipt(qty=8, do_not_submit=True) pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
pr1.append("items", { pr1.append("items", {
@ -689,8 +756,14 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEquals(pi2.items[0].qty, 2) self.assertEquals(pi2.items[0].qty, 2)
self.assertEquals(pi2.items[1].qty, 1) self.assertEquals(pi2.items[1].qty, 1)
pr2.cancel()
pi1.cancel()
pr1.reload()
pr1.cancel()
def test_stock_transfer_from_purchase_receipt(self): def test_stock_transfer_from_purchase_receipt(self):
pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', company="_Test Company with perpetual inventory") pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1) warehouse = "Stores - TCP1", do_not_save=1)
@ -713,18 +786,20 @@ class TestPurchaseReceipt(unittest.TestCase):
for sle in sl_entries: for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty) self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
def test_stock_transfer_from_purchase_receipt_with_valuation(self): pr.cancel()
warehouse = frappe.get_doc('Warehouse', 'Work In Progress - TCP1') pr1.cancel()
warehouse.account = '_Test Account Stock In Hand - TCP1'
warehouse.save()
pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', def test_stock_transfer_from_purchase_receipt_with_valuation(self):
create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
properties={"account": '_Test Account Stock In Hand - TCP1'})
pr1 = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
company="_Test Company with perpetual inventory") company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1) warehouse = "Stores - TCP1", do_not_save=1)
pr.items[0].from_warehouse = 'Work In Progress - TCP1' pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1'
pr.supplier_warehouse = '' pr.supplier_warehouse = ''
@ -749,7 +824,7 @@ class TestPurchaseReceipt(unittest.TestCase):
] ]
expected_sle = { expected_sle = {
'Work In Progress - TCP1': -5, '_Test Warehouse for Valuation - TCP1': -5,
'Stores - TCP1': 5 'Stores - TCP1': 5
} }
@ -761,60 +836,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(gle.debit, expected_gle[i][1]) self.assertEqual(gle.debit, expected_gle[i][1])
self.assertEqual(gle.credit, expected_gle[i][2]) self.assertEqual(gle.credit, expected_gle[i][2])
warehouse.account = '' pr.cancel()
warehouse.save() pr1.cancel()
def test_backdated_purchase_receipt(self):
# make purchase receipt for default company
make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
# try to make another backdated PR
posting_date = add_days(today(), -1)
pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
do_not_submit=True)
pr.set_posting_time = 1
pr.posting_date = posting_date
pr.save()
self.assertRaises(frappe.ValidationError, pr.submit)
# make purchase receipt for other company backdated
pr = make_purchase_receipt(company="_Test Company 5", warehouse="Stores - _TC5",
do_not_submit=True)
pr.set_posting_time = 1
pr.posting_date = posting_date
pr.submit()
# Allowed to submit for other company's PR
self.assertEqual(pr.docstatus, 1)
def test_backdated_purchase_receipt_for_same_company_different_warehouse(self):
# make purchase receipt for default company
make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
# try to make another backdated PR
posting_date = add_days(today(), -1)
pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
do_not_submit=True)
pr.set_posting_time = 1
pr.posting_date = posting_date
pr.save()
self.assertRaises(frappe.ValidationError, pr.submit)
# make purchase receipt for other company backdated
pr = make_purchase_receipt(company="_Test Company 4", warehouse="Finished Goods - _TC4",
do_not_submit=True)
pr.set_posting_time = 1
pr.posting_date = posting_date
pr.submit()
# Allowed to submit for other company's PR
self.assertEqual(pr.docstatus, 1)
def test_subcontracted_pr_for_multi_transfer_batches(self): def test_subcontracted_pr_for_multi_transfer_batches(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@ -877,6 +901,12 @@ class TestPurchaseReceipt(unittest.TestCase):
update_backflush_based_on("BOM") update_backflush_based_on("BOM")
pr.delete()
se.cancel()
ste2.cancel()
ste1.cancel()
po.cancel()
def get_sl_entries(voucher_type, voucher_no): def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
@ -972,6 +1002,8 @@ def make_purchase_receipt(**args):
pr.posting_date = args.posting_date or today() pr.posting_date = args.posting_date or today()
if args.posting_time: if args.posting_time:
pr.posting_time = args.posting_time pr.posting_time = args.posting_time
if args.posting_date or args.posting_time:
pr.set_posting_time = 1
pr.company = args.company or "_Test Company" pr.company = args.company or "_Test Company"
pr.supplier = args.supplier or "_Test Supplier" pr.supplier = args.supplier or "_Test Supplier"
pr.is_subcontracted = args.is_subcontracted or "No" pr.is_subcontracted = args.is_subcontracted or "No"

View File

@ -866,7 +866,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-11-02 10:00:38.204294", "modified": "2020-12-07 10:00:38.204294",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

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

@ -0,0 +1,52 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Repost Item Valuation', {
setup: function(frm) {
frm.set_query("warehouse", () => {
let filters = {
'is_group': 0
};
if (frm.doc.company) filters['company'] = frm.doc.company;
return {filters: filters};
});
frm.set_query("voucher_type", () => {
return {
filters: {
name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note',
'Sales Invoice', 'Stock Entry', 'Stock Reconciliation']]
}
};
});
if (frm.doc.company) {
frm.set_query("voucher_no", () => {
return {
filters: {
company: frm.doc.company
}
};
});
}
},
refresh: function(frm) {
if (frm.doc.status == "Failed") {
frm.add_custom_button(__('Restart'), function () {
frm.trigger("restart_reposting");
}).addClass("btn-primary");
}
},
restart_reposting: function(frm) {
frappe.call({
method: "restart_reposting",
doc: frm.doc,
callback: function(r) {
if (!r.exc) {
frm.refresh();
}
}
});
}
});

View File

@ -0,0 +1,215 @@
{
"actions": [],
"autoname": "REPOST-ITEM-VAL-.######",
"creation": "2020-10-22 22:27:07.742161",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"based_on",
"voucher_type",
"voucher_no",
"item_code",
"warehouse",
"posting_date",
"posting_time",
"column_break_5",
"status",
"company",
"allow_negative_stock",
"via_landed_cost_voucher",
"allow_zero_rate",
"amended_from",
"error_section",
"error_log"
],
"fields": [
{
"depends_on": "eval:doc.based_on=='Item and Warehouse'",
"fieldname": "item_code",
"fieldtype": "Link",
"label": "Item Code",
"mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
"options": "Item"
},
{
"depends_on": "eval:doc.based_on=='Item and Warehouse'",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
"options": "Warehouse"
},
{
"fetch_from": "voucher_no.posting_date",
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date",
"reqd": 1
},
{
"fetch_from": "voucher_no.posting_time",
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time"
},
{
"default": "Queued",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Queued\nIn Progress\nCompleted\nFailed",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Repost Item Valuation",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.status=='Failed'",
"fieldname": "error_section",
"fieldtype": "Section Break",
"label": "Error"
},
{
"fieldname": "error_log",
"fieldtype": "Long Text",
"label": "Error Log",
"no_copy": 1,
"read_only": 1
},
{
"fetch_from": "warehouse.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"depends_on": "eval:doc.based_on=='Transaction'",
"fieldname": "voucher_type",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher Type",
"mandatory_depends_on": "eval:doc.based_on=='Transaction'",
"options": "DocType"
},
{
"depends_on": "eval:doc.based_on=='Transaction'",
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"mandatory_depends_on": "eval:doc.based_on=='Transaction'",
"options": "voucher_type"
},
{
"default": "Transaction",
"fieldname": "based_on",
"fieldtype": "Select",
"label": "Based On",
"options": "Transaction\nItem and Warehouse",
"reqd": 1
},
{
"default": "0",
"fieldname": "allow_negative_stock",
"fieldtype": "Check",
"label": "Allow Negative Stock"
},
{
"default": "0",
"fieldname": "via_landed_cost_voucher",
"fieldtype": "Check",
"label": "Via Landed Cost Voucher"
},
{
"default": "0",
"fieldname": "allow_zero_rate",
"fieldtype": "Check",
"label": "Allow Zero Rate"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-12-10 07:52:12.476589",
"modified_by": "Administrator",
"module": "Stock",
"name": "Repost Item Valuation",
"owner": "Administrator",
"permissions": [
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Stock Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,112 @@
# -*- 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, erpnext
from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form
from erpnext.stock.stock_ledger import repost_future_sle
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):
def validate(self):
self.set_status()
self.reset_field_values()
self.set_company()
def reset_field_values(self):
if self.based_on == 'Transaction':
self.item_code = None
self.warehouse = None
else:
self.voucher_type = None
self.voucher_no = None
def set_company(self):
if self.voucher_type and self.voucher_no:
self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
elif self.warehouse:
self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
def set_status(self, status=None):
if not status:
status = 'Queued'
self.db_set('status', status)
def on_submit(self):
frappe.enqueue(repost, timeout=1800, queue='long',
job_name='repost_sle', now=frappe.flags.in_test, doc=self)
def restart_reposting(self):
self.set_status('Queued')
frappe.enqueue(repost, timeout=1800, queue='long',
job_name='repost_sle', now=True, doc=self)
def repost(doc):
try:
doc.set_status('In Progress')
frappe.db.commit()
repost_sl_entries(doc)
repost_gl_entries(doc)
check_if_stock_and_account_balance_synced(doc.posting_date, doc.company)
doc.set_status('Completed')
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(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')
raise
finally:
frappe.db.commit()
def repost_sl_entries(doc):
if doc.based_on == 'Transaction':
repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no,
allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
else:
repost_future_sle(args=[frappe._dict({
"item_code": doc.item_code,
"warehouse": doc.warehouse,
"posting_date": doc.posting_date,
"posting_time": doc.posting_time
})], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
def repost_gl_entries(doc):
if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
return
if doc.based_on == 'Transaction':
ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
items, warehouses = ref_doc.get_items_and_warehouses()
else:
items = [doc.item_code]
warehouses = [doc.warehouse]
update_gl_entries_after(doc.posting_date, doc.posting_time,
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

@ -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 TestRepostItemValuation(unittest.TestCase):
pass

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
@ -134,17 +134,13 @@ class SerialNo(StockController):
sle_dict = self.get_stock_ledger_entries(serial_no) sle_dict = self.get_stock_ledger_entries(serial_no)
if sle_dict: if sle_dict:
if sle_dict.get("incoming", []): if sle_dict.get("incoming", []):
sle_list = [sle for sle in sle_dict["incoming"] if sle.is_cancelled == 0] entries["purchase_sle"] = sle_dict["incoming"][0]
if sle_list:
entries["purchase_sle"] = sle_list[0]
if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0: if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
entries["last_sle"] = sle_dict["incoming"][0] entries["last_sle"] = sle_dict["incoming"][0]
else: else:
entries["last_sle"] = sle_dict["outgoing"][0] entries["last_sle"] = sle_dict["outgoing"][0]
sle_list = [sle for sle in sle_dict["outgoing"] if sle.is_cancelled == 0] entries["delivery_sle"] = sle_dict["outgoing"][0]
if sle_list:
entries["delivery_sle"] = sle_list[0]
return entries return entries
@ -155,11 +151,12 @@ class SerialNo(StockController):
for sle in frappe.db.sql(""" for sle in frappe.db.sql("""
SELECT voucher_type, voucher_no, SELECT voucher_type, voucher_no,
posting_date, posting_time, incoming_rate, actual_qty, serial_no, is_cancelled posting_date, posting_time, incoming_rate, actual_qty, serial_no
FROM FROM
`tabStock Ledger Entry` `tabStock Ledger Entry`
WHERE WHERE
item_code=%s AND company = %s item_code=%s AND company = %s
AND is_cancelled = 0
AND (serial_no = %s AND (serial_no = %s
OR serial_no like %s OR serial_no like %s
OR serial_no like %s OR serial_no like %s
@ -179,7 +176,7 @@ class SerialNo(StockController):
def on_trash(self): def on_trash(self):
sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry` sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry`
where serial_no like %s and item_code=%s""", where serial_no like %s and item_code=%s and is_cancelled=0""",
("%%%s%%" % self.name, self.item_code), as_dict=True) ("%%%s%%" % self.name, self.item_code), as_dict=True)
# Find the exact match # Find the exact match
@ -229,7 +226,7 @@ def validate_serial_no(sle, item_det):
if serial_nos: if serial_nos:
frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code), frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
SerialNoNotRequiredError) SerialNoNotRequiredError)
else: elif not sle.is_cancelled:
if serial_nos: if serial_nos:
if cint(sle.actual_qty) != flt(sle.actual_qty): if cint(sle.actual_qty) != flt(sle.actual_qty):
frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty)) frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty))
@ -244,21 +241,18 @@ 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 and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
.format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse), SerialNoWarehouseError)
if sr.item_code!=sle.item_code: if sr.item_code!=sle.item_code:
if not allow_serial_nos_with_different_item(serial_no, sle): if not allow_serial_nos_with_different_item(serial_no, sle):
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):
@ -277,7 +271,7 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no, frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no,
sle.batch_no), SerialNoBatchError) sle.batch_no), SerialNoBatchError)
if not sr.warehouse: if not sle.is_cancelled and not sr.warehouse:
frappe.throw(_("Serial No {0} does not belong to any Warehouse") frappe.throw(_("Serial No {0} does not belong to any Warehouse")
.format(serial_no), SerialNoWarehouseError) .format(serial_no), SerialNoWarehouseError)
@ -327,6 +321,12 @@ def validate_serial_no(sle, item_det):
elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series: elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series:
frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code), frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
SerialNoRequiredError) SerialNoRequiredError)
elif serial_nos:
for serial_no in serial_nos:
sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1)
if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
.format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse))
def validate_material_transfer_entry(sle_doc): def validate_material_transfer_entry(sle_doc):
sle_doc.update({ sle_doc.update({
@ -334,7 +334,7 @@ def validate_material_transfer_entry(sle_doc):
"skip_serial_no_validaiton": False "skip_serial_no_validaiton": False
}) })
if (sle_doc.voucher_type == "Stock Entry" and if (sle_doc.voucher_type == "Stock Entry" and not sle_doc.is_cancelled and
frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"): frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"):
if sle_doc.actual_qty < 0: if sle_doc.actual_qty < 0:
sle_doc.skip_update_serial_no = True sle_doc.skip_update_serial_no = True
@ -349,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
@ -359,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
@ -379,7 +380,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle):
stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no) stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no)
if stock_entry.purpose in ("Repack", "Manufacture"): if stock_entry.purpose in ("Repack", "Manufacture"):
for d in stock_entry.get("items"): for d in stock_entry.get("items"):
if d.serial_no and (d.s_warehouse or d.t_warehouse): if d.serial_no and (d.s_warehouse if not sle.is_cancelled else d.t_warehouse):
serial_nos = get_serial_nos(d.serial_no) serial_nos = get_serial_nos(d.serial_no)
if sle_serial_no in serial_nos: if sle_serial_no in serial_nos:
allow_serial_nos = True allow_serial_nos = True
@ -388,7 +389,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle):
def update_serial_nos(sle, item_det): def update_serial_nos(sle, item_det):
if sle.skip_update_serial_no: return if sle.skip_update_serial_no: return
if not sle.serial_no and cint(sle.actual_qty) > 0 \ if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \
and item_det.has_serial_no == 1 and item_det.serial_no_series: and item_det.has_serial_no == 1 and item_det.serial_no_series:
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty) serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
frappe.db.set(sle, "serial_no", serial_nos) frappe.db.set(sle, "serial_no", serial_nos)
@ -420,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

@ -12,7 +12,6 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
test_dependencies = ["Item"] test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No') test_records = frappe.get_test_records('Serial No')
@ -38,8 +37,6 @@ class TestSerialNo(unittest.TestCase):
self.assertTrue(SerialNoCannotCannotChangeError, sr.save) self.assertTrue(SerialNoCannotCannotChangeError, sr.save)
def test_inter_company_transfer(self): def test_inter_company_transfer(self):
set_perpetual_inventory(0, "_Test Company 1")
set_perpetual_inventory(0)
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = get_serial_nos(se.get("items")[0].serial_no) serial_nos = get_serial_nos(se.get("items")[0].serial_no)

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

@ -510,22 +510,31 @@ frappe.ui.form.on('Stock Entry', {
calculate_amount: function(frm) { calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm); frm.events.calculate_total_additional_costs(frm);
let total_basic_amount = 0;
const total_basic_amount = frappe.utils.sum( if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
(frm.doc.items || []).map(function(i) { return i.t_warehouse ? flt(i.basic_amount) : 0; }) total_basic_amount = frappe.utils.sum(
); (frm.doc.items || []).map(function(i) {
return i.is_finished_item ? flt(i.basic_amount) : 0;
})
);
} else {
total_basic_amount = frappe.utils.sum(
(frm.doc.items || []).map(function(i) {
return i.t_warehouse ? flt(i.basic_amount) : 0;
})
);
}
for (let i in frm.doc.items) { for (let i in frm.doc.items) {
let item = frm.doc.items[i]; let item = frm.doc.items[i];
if (item.t_warehouse && total_basic_amount) { if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) {
item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs; item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs;
} else { } else {
item.additional_cost = 0; item.additional_cost = 0;
} }
item.amount = flt(item.basic_amount + flt(item.additional_cost), item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
precision("amount", item));
if (flt(item.transfer_qty)) { if (flt(item.transfer_qty)) {
item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)), item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),

View File

@ -644,9 +644,10 @@
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-11 19:10:07.954981", "modified": "2020-09-09 12:59:02.508943",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry", "name": "Stock Entry",

View File

@ -18,7 +18,7 @@ from erpnext.stock.utils import get_bin
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 update_serial_nos_after_submit, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
from erpnext.accounts.general_ledger import process_gl_map
import json import json
from six import string_types, itervalues, iteritems from six import string_types, itervalues, iteritems
@ -58,6 +58,7 @@ class StockEntry(StockController):
self.validate_warehouse() self.validate_warehouse()
self.validate_work_order() self.validate_work_order()
self.validate_bom() self.validate_bom()
self.mark_finished_and_scrap_items()
self.validate_finished_goods() self.validate_finished_goods()
self.validate_with_material_request() self.validate_with_material_request()
self.validate_batch() self.validate_batch()
@ -75,13 +76,11 @@ class StockEntry(StockController):
else: else:
set_batch_nos(self, 's_warehouse') set_batch_nos(self, 's_warehouse')
self.set_incoming_rate()
self.validate_serialized_batch() self.validate_serialized_batch()
self.set_actual_qty() self.set_actual_qty()
self.calculate_rate_and_amount(update_finished_item_rate=False) self.calculate_rate_and_amount()
def on_submit(self): def on_submit(self):
self.update_stock_ledger() self.update_stock_ledger()
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
@ -89,11 +88,15 @@ class StockEntry(StockController):
self.validate_purchase_order() self.validate_purchase_order()
if self.purchase_order and self.purpose == "Send to Subcontractor": if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items() self.update_purchase_order_supplied_items()
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle()
self.update_cost_in_project() self.update_cost_in_project()
self.validate_reserved_serial_no_consumption() self.validate_reserved_serial_no_consumption()
self.update_transferred_qty() self.update_transferred_qty()
self.update_quality_inspection() self.update_quality_inspection()
if self.work_order and self.purpose == "Manufacture": if self.work_order and self.purpose == "Manufacture":
self.update_so_in_serial_number() self.update_so_in_serial_number()
@ -113,9 +116,10 @@ class StockEntry(StockController):
self.update_work_order() self.update_work_order()
self.update_stock_ledger() self.update_stock_ledger()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
self.update_cost_in_project() self.update_cost_in_project()
self.update_transferred_qty() self.update_transferred_qty()
self.update_quality_inspection() self.update_quality_inspection()
@ -255,12 +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:
production_item = frappe.get_value('Work Order', self.work_order, 'production_item') for d in self.items:
for item in self.items: if d.is_finished_item:
if item.item_code == production_item and item.t_warehouse and item.qty != self.fg_completed_qty: item_wise_qty.setdefault(d.item_code, []).append(d.qty)
frappe.throw(_("Finished product quantity <b>{0}</b> and For Quantity <b>{1}</b> cannot be different")
.format(item.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)):
@ -316,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))
@ -382,21 +390,6 @@ class StockEntry(StockController):
frappe.throw(_("Stock Entries already created for Work Order ") frappe.throw(_("Stock Entries already created for Work Order ")
+ self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError) + self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError)
def set_incoming_rate(self):
if self.purpose == "Repack":
self.set_basic_rate_for_finished_goods()
for d in self.items:
if d.s_warehouse:
args = self.get_args_for_incoming_rate(d)
d.basic_rate = get_incoming_rate(args)
elif d.allow_zero_valuation_rate and not d.s_warehouse:
d.basic_rate = 0.0
elif d.t_warehouse and not d.basic_rate:
d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
self.doctype, self.name, d.allow_zero_valuation_rate,
currency=erpnext.get_company_currency(self.company), company=self.company)
def set_actual_qty(self): def set_actual_qty(self):
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
@ -432,57 +425,65 @@ class StockEntry(StockController):
d.serial_no = transferred_serial_no d.serial_no = transferred_serial_no
def get_stock_and_rate(self): def get_stock_and_rate(self):
"""
Updates rate and availability of all the items.
Called from Update Rate and Availability button.
"""
self.set_work_order_details() self.set_work_order_details()
self.set_transfer_qty() self.set_transfer_qty()
self.set_actual_qty() self.set_actual_qty()
self.calculate_rate_and_amount() self.calculate_rate_and_amount()
def calculate_rate_and_amount(self, force=False, def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
update_finished_item_rate=True, raise_error_if_no_rate=True): self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate)
self.set_basic_rate(force, update_finished_item_rate, raise_error_if_no_rate)
self.distribute_additional_costs() self.distribute_additional_costs()
self.update_valuation_rate() self.update_valuation_rate()
self.set_total_incoming_outgoing_value() self.set_total_incoming_outgoing_value()
self.set_total_amount() self.set_total_amount()
def set_basic_rate(self, force=False, update_finished_item_rate=True, raise_error_if_no_rate=True): def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
"""get stock and incoming rate on posting date""" """
raw_material_cost = 0.0 Set rate for outgoing, scrapped and finished items
scrap_material_cost = 0.0 """
fg_basic_rate = 0.0 # Set rate for outgoing items
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
for d in self.get('items'): for d in self.get('items'):
if d.t_warehouse: fg_basic_rate = flt(d.basic_rate) if d.s_warehouse or d.set_basic_rate_manually: continue
args = self.get_args_for_incoming_rate(d)
# get basic rate if d.allow_zero_valuation_rate:
if not d.bom_no: d.basic_rate = 0.0
if (not flt(d.basic_rate) and not d.allow_zero_valuation_rate) or d.s_warehouse or force: elif d.is_finished_item:
basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate), self.precision("basic_rate", d)) if self.purpose == "Manufacture":
if basic_rate > 0: d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost)
d.basic_rate = basic_rate elif self.purpose == "Repack":
d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost)
if not d.basic_rate and not d.allow_zero_valuation_rate:
d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
self.doctype, self.name, d.allow_zero_valuation_rate,
currency=erpnext.get_company_currency(self.company), company=self.company,
raise_error_if_no_rate=raise_error_if_no_rate)
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True):
outgoing_items_cost = 0.0
for d in self.get('items'):
if d.s_warehouse:
if reset_outgoing_rate:
args = self.get_args_for_incoming_rate(d)
rate = get_incoming_rate(args)
if rate > 0:
d.basic_rate = rate
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse: if not d.t_warehouse:
raw_material_cost += flt(d.basic_amount) outgoing_items_cost += flt(d.basic_amount)
return outgoing_items_cost
# get scrap items basic rate
if d.bom_no:
if not flt(d.basic_rate) and not d.allow_zero_valuation_rate and \
getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate),
self.precision("basic_rate", d))
if basic_rate > 0:
d.basic_rate = basic_rate
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
scrap_material_cost += flt(d.basic_amount)
number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse])
if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate:
self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost)
def get_args_for_incoming_rate(self, item): def get_args_for_incoming_rate(self, item):
return frappe._dict({ return frappe._dict({
@ -498,44 +499,44 @@ class StockEntry(StockController):
"allow_zero_valuation": item.allow_zero_valuation_rate, "allow_zero_valuation": item.allow_zero_valuation_rate,
}) })
def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0): def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
total_fg_qty = 0 finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
if not raw_material_cost and self.get("items"): if len(finished_items) == 1:
raw_material_cost = sum([flt(row.basic_amount) for row in self.items return flt(outgoing_items_cost / finished_item_qty)
if row.s_warehouse and not row.t_warehouse]) else:
unique_finished_items = set(finished_items)
if len(unique_finished_items) == 1:
total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
return flt(outgoing_items_cost / total_fg_qty)
total_fg_qty = sum([flt(row.qty) for row in self.items def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
if row.t_warehouse and not row.s_warehouse]) scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
if self.purpose in ["Manufacture", "Repack"]: # Get raw materials cost from BOM if multiple material consumption entries
for d in self.get("items"): if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
if (d.transfer_qty and (d.bom_no or d.t_warehouse) bom_items = self.get_bom_raw_materials(finished_item_qty)
and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)): outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
if (self.work_order and self.purpose == "Manufacture" return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)
and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")):
bom_items = self.get_bom_raw_materials(d.transfer_qty)
raw_material_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
if raw_material_cost and self.purpose == "Manufacture":
d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate"))
d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
d.basic_amount = d.basic_rate * flt(d.qty)
def distribute_additional_costs(self): def distribute_additional_costs(self):
if self.purpose == "Material Issue": # If no incoming items, set additional costs blank
if not any([d.item_code for d in self.items if d.t_warehouse]):
self.additional_costs = [] self.additional_costs = []
self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")]) self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")])
total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
for d in self.get("items"): if self.purpose in ("Repack", "Manufacture"):
if d.t_warehouse and total_basic_amount: incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
d.additional_cost = (flt(d.basic_amount) / total_basic_amount) * self.total_additional_costs else:
else: incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
d.additional_cost = 0
if incoming_items_cost:
for d in self.get("items"):
if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse:
d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
else:
d.additional_cost = 0
def update_valuation_rate(self): def update_valuation_rate(self):
for d in self.get("items"): for d in self.get("items"):
@ -638,71 +639,115 @@ class StockEntry(StockController):
item_code = d.original_item or d.item_code item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no) validate_bom_no(item_code, d.bom_no)
def mark_finished_and_scrap_items(self):
if self.purpose in ("Repack", "Manufacture"):
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
return
finished_item = self.get_finished_item()
for d in self.items:
if d.t_warehouse and not d.s_warehouse:
if self.purpose=="Repack" or d.item_code == finished_item:
d.is_finished_item = 1
else:
d.is_scrap_item = 1
else:
d.is_finished_item = 0
d.is_scrap_item = 0
def get_finished_item(self):
finished_item = None
if self.work_order:
finished_item = frappe.db.get_value("Work Order", self.work_order, "production_item")
elif self.bom_no:
finished_item = frappe.db.get_value("BOM", self.bom_no, "item")
return finished_item
def validate_finished_goods(self): def validate_finished_goods(self):
"""validation: finished good quantity should be same as manufacturing quantity""" """validation: finished good quantity should be same as manufacturing quantity"""
if not self.work_order: return if not self.work_order: return
items_with_target_warehouse = []
allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))
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"])
finished_items = []
for d in self.get('items'): for d in self.get('items'):
if (self.purpose != "Send to Subcontractor" and d.bom_no if d.is_finished_item:
and flt(d.transfer_qty) > flt(self.fg_completed_qty) and d.item_code == production_item): if d.item_code != production_item:
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \ frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
format(d.idx, d.transfer_qty, self.fg_completed_qty)) .format(d.item_code, self.work_order))
elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
format(d.idx, d.transfer_qty, self.fg_completed_qty))
finished_items.append(d.item_code)
if self.work_order and self.purpose == "Manufacture" and d.t_warehouse: if len(set(finished_items)) > 1:
items_with_target_warehouse.append(d.item_code) frappe.throw(_("Multiple items cannot be marked as finished item"))
if self.purpose == "Manufacture":
allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))
if self.work_order and self.purpose == "Manufacture":
allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty) allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty)
if self.fg_completed_qty > allowed_qty: if self.fg_completed_qty > allowed_qty:
frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}") frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}")
.format(flt(self.fg_completed_qty), wo_qty)) .format(flt(self.fg_completed_qty), wo_qty))
if production_item not in items_with_target_warehouse:
frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry")
.format(production_item))
def update_stock_ledger(self): def update_stock_ledger(self):
sl_entries = [] sl_entries = []
finished_item_row = self.get_finished_item_row()
# make sl entries for source warehouse first, then do for target warehouse # make sl entries for source warehouse first
for d in self.get('items'): self.get_sle_for_source_warehouse(sl_entries, finished_item_row)
if cstr(d.s_warehouse):
sl_entries.append(self.get_sl_entries(d, {
"warehouse": cstr(d.s_warehouse),
"actual_qty": -flt(d.transfer_qty),
"incoming_rate": 0
}))
for d in self.get('items'): # SLE for target warehouse
if cstr(d.t_warehouse): self.get_sle_for_target_warehouse(sl_entries, finished_item_row)
sl_entries.append(self.get_sl_entries(d, {
"warehouse": cstr(d.t_warehouse),
"actual_qty": flt(d.transfer_qty),
"incoming_rate": flt(d.valuation_rate)
}))
# On cancellation, make stock ledger entry for
# target warehouse first, to update serial no values properly
# if cstr(d.s_warehouse) and self.docstatus == 2:
# sl_entries.append(self.get_sl_entries(d, {
# "warehouse": cstr(d.s_warehouse),
# "actual_qty": -flt(d.transfer_qty),
# "incoming_rate": 0
# }))
# reverse sl entries if cancel
if self.docstatus == 2: if self.docstatus == 2:
sl_entries.reverse() sl_entries.reverse()
self.make_sl_entries(sl_entries) self.make_sl_entries(sl_entries)
def get_finished_item_row(self):
finished_item_row = None
if self.purpose in ("Manufacture", "Repack"):
for d in self.get('items'):
if d.is_finished_item:
finished_item_row = d
return finished_item_row
def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
for d in self.get('items'):
if cstr(d.s_warehouse):
sle = self.get_sl_entries(d, {
"warehouse": cstr(d.s_warehouse),
"actual_qty": -flt(d.transfer_qty),
"incoming_rate": 0
})
if cstr(d.t_warehouse):
sle.dependant_sle_voucher_detail_no = d.name
elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse):
sle.dependant_sle_voucher_detail_no = finished_item_row.name
sl_entries.append(sle)
def get_sle_for_target_warehouse(self, sl_entries, finished_item_row):
for d in self.get('items'):
if cstr(d.t_warehouse):
sle = self.get_sl_entries(d, {
"warehouse": cstr(d.t_warehouse),
"actual_qty": flt(d.transfer_qty),
"incoming_rate": flt(d.valuation_rate)
})
if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name):
sle.recalculate_rate = 1
sl_entries.append(sle)
def get_gl_entries(self, warehouse_account): def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
@ -747,7 +792,7 @@ class StockEntry(StockController):
"credit": -1 * amount # put it as negative credit instead of debit purposefully "credit": -1 * amount # put it as negative credit instead of debit purposefully
}, item=d)) }, item=d))
return gl_entries return process_gl_map(gl_entries)
def update_work_order(self): def update_work_order(self):
def _validate_work_order(pro_doc): def _validate_work_order(pro_doc):
@ -996,6 +1041,7 @@ class StockEntry(StockController):
"stock_uom": item.stock_uom, "stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"), "expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"), "cost_center": item.get("buying_cost_center"),
"is_finished_item": 1
} }
}, bom_no = self.bom_no) }, bom_no = self.bom_no)
@ -1034,6 +1080,7 @@ class StockEntry(StockController):
for item in itervalues(item_dict): for item in itervalues(item_dict):
item.from_warehouse = "" item.from_warehouse = ""
item.is_scrap_item = 1
return item_dict return item_dict
def get_unconsumed_raw_materials(self): def get_unconsumed_raw_materials(self):
@ -1246,6 +1293,8 @@ class StockEntry(StockController):
se_child.subcontracted_item = item_dict[d].get("main_item_code") se_child.subcontracted_item = item_dict[d].get("main_item_code")
se_child.cost_center = (item_dict[d].get("cost_center") or se_child.cost_center = (item_dict[d].get("cost_center") or
get_default_cost_center(item_dict[d], company = self.company)) get_default_cost_center(item_dict[d], company = self.company))
se_child.is_finished_item = item_dict[d].get("is_finished_item", 0)
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item", for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name"]: "expense_account", "description", "item_name"]:

View File

@ -6,7 +6,6 @@ import frappe, unittest
import frappe.defaults import frappe.defaults
from frappe.utils import flt, nowdate, nowtime from frappe.utils import flt, nowdate, nowtime
from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
from frappe.permissions import add_user_permission, remove_user_permission from frappe.permissions import add_user_permission, remove_user_permission
@ -32,7 +31,6 @@ def get_sle(**args):
class TestStockEntry(unittest.TestCase): class TestStockEntry(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
set_perpetual_inventory(0)
def test_fifo(self): def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@ -181,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",
@ -204,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],
]) ])
) )
@ -213,7 +209,6 @@ class TestStockEntry(unittest.TestCase):
def test_repack_no_change_in_valuation(self): def test_repack_no_change_in_valuation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
set_perpetual_inventory(0, company)
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
@ -235,8 +230,6 @@ class TestStockEntry(unittest.TestCase):
order by account desc""", repack.name, as_dict=1) order by account desc""", repack.name, as_dict=1)
self.assertFalse(gl_entries) self.assertFalse(gl_entries)
set_perpetual_inventory(0, repack.company)
def test_repack_with_additional_costs(self): def test_repack_with_additional_costs(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
@ -474,7 +467,6 @@ class TestStockEntry(unittest.TestCase):
def test_warehouse_company_validation(self): def test_warehouse_company_validation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company') company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
set_perpetual_inventory(0, company)
frappe.get_doc("User", "test2@example.com")\ frappe.get_doc("User", "test2@example.com")\
.add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager") .add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager")
frappe.set_user("test2@example.com") frappe.set_user("test2@example.com")
@ -500,7 +492,7 @@ class TestStockEntry(unittest.TestCase):
st1 = frappe.copy_doc(test_records[0]) st1 = frappe.copy_doc(test_records[0])
st1.company = "_Test Company 1" st1.company = "_Test Company 1"
set_perpetual_inventory(0, st1.company)
frappe.set_user("test@example.com") frappe.set_user("test@example.com")
st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1" st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
self.assertRaises(frappe.PermissionError, st1.insert) self.assertRaises(frappe.PermissionError, st1.insert)
@ -698,47 +690,54 @@ class TestStockEntry(unittest.TestCase):
repack.insert() repack.insert()
self.assertRaises(frappe.ValidationError, repack.submit) self.assertRaises(frappe.ValidationError, repack.submit)
def test_material_consumption(self): # def test_material_consumption(self):
from erpnext.manufacturing.doctype.work_order.work_order \ # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
import make_stock_entry as _make_stock_entry # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
"is_default": 1, "docstatus": 1})
work_order = frappe.new_doc("Work Order") # from erpnext.manufacturing.doctype.work_order.work_order \
work_order.update({ # import make_stock_entry as _make_stock_entry
"company": "_Test Company", # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
"fg_warehouse": "_Test Warehouse 1 - _TC", # "is_default": 1, "docstatus": 1})
"production_item": "_Test FG Item 2",
"bom_no": bom_no,
"qty": 4.0,
"stock_uom": "_Test UOM",
"wip_warehouse": "_Test Warehouse - _TC",
"additional_operating_cost": 1000
})
work_order.insert()
work_order.submit()
make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100) # work_order = frappe.new_doc("Work Order")
make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20) # work_order.update({
# "company": "_Test Company",
# "fg_warehouse": "_Test Warehouse 1 - _TC",
# "production_item": "_Test FG Item 2",
# "bom_no": bom_no,
# "qty": 4.0,
# "stock_uom": "_Test UOM",
# "wip_warehouse": "_Test Warehouse - _TC",
# "additional_operating_cost": 1000,
# "use_multi_level_bom": 1
# })
# work_order.insert()
# work_order.submit()
item_quantity = { # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
'_Test Item': 10.0, # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
'_Test Item 2': 12.0,
'_Test Serialized Item With Series': 6.0
}
stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2)) # item_quantity = {
for d in stock_entry.get('items'): # '_Test Item': 2.0,
self.assertEqual(item_quantity.get(d.item_code), d.qty) # '_Test Item 2': 12.0,
# '_Test Serialized Item With Series': 6.0
# }
# stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
# for d in stock_entry.get('items'):
# self.assertEqual(item_quantity.get(d.item_code), d.qty)
def test_customer_provided_parts_se(self): def test_customer_provided_parts_se(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC") se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt',
qty=4, to_warehouse = "_Test Warehouse - _TC")
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0) self.assertEqual(se.get("items")[0].amount, 0)
def test_gle_for_opening_stock_entry(self): def test_gle_for_opening_stock_entry(self):
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
self.assertRaises(OpeningEntryAccountError, mr.save) self.assertRaises(OpeningEntryAccountError, mr.save)
@ -753,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":"Basil Leaves", "item_code":"_Test Item",
"description":"Basil Leaves", "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":"Basil Leaves", "item_code":"_Test Item",
"description":"Basil Leaves", "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

@ -13,8 +13,10 @@
"t_warehouse", "t_warehouse",
"sec_break1", "sec_break1",
"item_code", "item_code",
"col_break2",
"item_name", "item_name",
"col_break2",
"is_finished_item",
"is_scrap_item",
"subcontracted_item", "subcontracted_item",
"section_break_8", "section_break_8",
"description", "description",
@ -22,35 +24,37 @@
"item_group", "item_group",
"image", "image",
"image_view", "image_view",
"quantity_and_rate", "quantity_section",
"set_basic_rate_manually",
"qty", "qty",
"basic_rate",
"basic_amount",
"additional_cost",
"amount",
"valuation_rate",
"col_break3",
"uom",
"conversion_factor",
"stock_uom",
"transfer_qty", "transfer_qty",
"retain_sample", "retain_sample",
"column_break_20",
"uom",
"stock_uom",
"conversion_factor",
"sample_quantity", "sample_quantity",
"rates_section",
"basic_rate",
"additional_cost",
"valuation_rate",
"allow_zero_valuation_rate",
"col_break3",
"set_basic_rate_manually",
"basic_amount",
"amount",
"serial_no_batch", "serial_no_batch",
"serial_no", "serial_no",
"col_break4", "col_break4",
"batch_no", "batch_no",
"quality_inspection",
"accounting", "accounting",
"expense_account", "expense_account",
"col_break5",
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"project",
"dimension_col_break", "dimension_col_break",
"more_info", "more_info",
"allow_zero_valuation_rate",
"actual_qty", "actual_qty",
"transferred_qty",
"bom_no", "bom_no",
"allow_alternative_item", "allow_alternative_item",
"col_break6", "col_break6",
@ -62,9 +66,8 @@
"ste_detail", "ste_detail",
"po_detail", "po_detail",
"column_break_51", "column_break_51",
"transferred_qty",
"reference_purchase_receipt", "reference_purchase_receipt",
"project" "quality_inspection"
], ],
"fields": [ "fields": [
{ {
@ -159,11 +162,6 @@
"options": "image", "options": "image",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
"label": "Quantity and Rate"
},
{ {
"bold": 1, "bold": 1,
"fieldname": "qty", "fieldname": "qty",
@ -321,10 +319,6 @@
"options": "Account", "options": "Account",
"print_hide": 1 "print_hide": 1
}, },
{
"fieldname": "col_break5",
"fieldtype": "Column Break"
},
{ {
"default": ":Company", "default": ":Company",
"depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))", "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
@ -335,6 +329,7 @@
"print_hide": 1 "print_hide": 1
}, },
{ {
"collapsible": 1,
"fieldname": "more_info", "fieldname": "more_info",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "More Information" "label": "More Information"
@ -456,6 +451,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"collapsible": 1,
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Dimensions" "label": "Accounting Dimensions"
@ -498,13 +494,39 @@
"fieldname": "set_basic_rate_manually", "fieldname": "set_basic_rate_manually",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Basic Rate Manually" "label": "Set Basic Rate Manually"
},
{
"fieldname": "quantity_section",
"fieldtype": "Section Break",
"label": "Quantity"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
},
{
"fieldname": "rates_section",
"fieldtype": "Section Break",
"label": "Rates"
},
{
"default": "0",
"fieldname": "is_scrap_item",
"fieldtype": "Check",
"label": "Is Scrap Item"
},
{
"default": "0",
"fieldname": "is_finished_item",
"fieldtype": "Check",
"label": "Is Finished Item"
} }
], ],
"idx": 1, "idx": 1,
"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

@ -8,26 +8,33 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"item_code", "item_code",
"serial_no",
"batch_no",
"warehouse", "warehouse",
"posting_date", "posting_date",
"posting_time", "posting_time",
"column_break_6",
"voucher_type", "voucher_type",
"voucher_no", "voucher_no",
"voucher_detail_no", "voucher_detail_no",
"dependant_sle_voucher_detail_no",
"recalculate_rate",
"section_break_11",
"actual_qty", "actual_qty",
"qty_after_transaction",
"incoming_rate", "incoming_rate",
"outgoing_rate", "outgoing_rate",
"stock_uom", "column_break_17",
"qty_after_transaction",
"valuation_rate", "valuation_rate",
"stock_value", "stock_value",
"stock_value_difference", "stock_value_difference",
"stock_queue", "stock_queue",
"project", "section_break_21",
"company", "company",
"stock_uom",
"project",
"batch_no",
"column_break_26",
"fiscal_year", "fiscal_year",
"serial_no",
"is_cancelled", "is_cancelled",
"to_rename" "to_rename"
], ],
@ -50,7 +57,6 @@
{ {
"fieldname": "serial_no", "fieldname": "serial_no",
"fieldtype": "Long Text", "fieldtype": "Long Text",
"in_list_view": 1,
"label": "Serial No", "label": "Serial No",
"print_width": "100px", "print_width": "100px",
"read_only": 1, "read_only": 1,
@ -59,7 +65,6 @@
{ {
"fieldname": "batch_no", "fieldname": "batch_no",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "Batch No", "label": "Batch No",
"oldfieldname": "batch_no", "oldfieldname": "batch_no",
"oldfieldtype": "Data", "oldfieldtype": "Data",
@ -119,6 +124,7 @@
"fieldname": "voucher_no", "fieldname": "voucher_no",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_filter": 1, "in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Voucher No", "label": "Voucher No",
"oldfieldname": "voucher_no", "oldfieldname": "voucher_no",
@ -142,6 +148,7 @@
"fieldname": "actual_qty", "fieldname": "actual_qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_filter": 1, "in_filter": 1,
"in_list_view": 1,
"label": "Actual Quantity", "label": "Actual Quantity",
"oldfieldname": "actual_qty", "oldfieldname": "actual_qty",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
@ -152,6 +159,7 @@
{ {
"fieldname": "incoming_rate", "fieldname": "incoming_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1,
"label": "Incoming Rate", "label": "Incoming Rate",
"oldfieldname": "incoming_rate", "oldfieldname": "incoming_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
@ -217,13 +225,11 @@
{ {
"fieldname": "stock_queue", "fieldname": "stock_queue",
"fieldtype": "Text", "fieldtype": "Text",
"hidden": 1,
"label": "Stock Queue (FIFO)", "label": "Stock Queue (FIFO)",
"oldfieldname": "fcfs_stack", "oldfieldname": "fcfs_stack",
"oldfieldtype": "Text", "oldfieldtype": "Text",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1
"report_hide": 1
}, },
{ {
"fieldname": "project", "fieldname": "project",
@ -269,14 +275,48 @@
"hidden": 1, "hidden": 1,
"label": "To Rename", "label": "To Rename",
"search_index": 1 "search_index": 1
},
{
"fieldname": "dependant_sle_voucher_detail_no",
"fieldtype": "Data",
"label": "Dependant SLE Voucher Detail No"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_21",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "recalculate_rate",
"fieldtype": "Check",
"label": "Recalculate Incoming/Outgoing Rate",
"no_copy": 1,
"read_only": 1
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"icon": "fa fa-list", "icon": "fa fa-list",
"idx": 1, "idx": 1,
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-04-23 05:57:03.985520", "modified": "2020-09-07 11:10:35.318872",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Ledger Entry", "name": "Stock Ledger Entry",

View File

@ -10,8 +10,10 @@ from frappe.model.document import Document
from datetime import date from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from frappe.core.doctype.role.role import get_users
class StockFreezeError(frappe.ValidationError): pass class StockFreezeError(frappe.ValidationError): pass
class BackDatedStockTransaction(frappe.ValidationError): pass
exclude_from_linked_with = True exclude_from_linked_with = True
@ -34,7 +36,6 @@ class StockLedgerEntry(Document):
self.validate_and_set_fiscal_year() self.validate_and_set_fiscal_year()
self.block_transactions_against_group_warehouse() self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time() self.validate_with_last_transaction_posting_time()
self.validate_future_posting()
def on_submit(self): def on_submit(self):
self.check_stock_frozen_date() self.check_stock_frozen_date()
@ -48,7 +49,7 @@ class StockLedgerEntry(Document):
def calculate_batch_qty(self): def calculate_batch_qty(self):
if self.batch_no: if self.batch_no:
batch_qty = frappe.db.get_value("Stock Ledger Entry", batch_qty = frappe.db.get_value("Stock Ledger Entry",
{"docstatus": 1, "batch_no": self.batch_no}, {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0},
"sum(actual_qty)") or 0 "sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
@ -88,14 +89,14 @@ class StockLedgerEntry(Document):
# check if batch number is required # check if batch number is required
if self.voucher_type != 'Stock Reconciliation': if self.voucher_type != 'Stock Reconciliation':
if item_det.has_batch_no ==1: if item_det.has_batch_no == 1:
batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name
if not self.batch_no: if not self.batch_no:
frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item)) frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}): elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item)) frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item))
elif item_det.has_batch_no ==0 and self.batch_no: elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code)) frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
if item_det.has_variants: if item_det.has_variants:
@ -142,28 +143,28 @@ class StockLedgerEntry(Document):
is_group_warehouse(self.warehouse) is_group_warehouse(self.warehouse)
def validate_with_last_transaction_posting_time(self): def validate_with_last_transaction_posting_time(self):
last_transaction_time = frappe.db.sql(""" authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions")
select MAX(timestamp(posting_date, posting_time)) as posting_time if authorized_role:
from `tabStock Ledger Entry` authorized_users = get_users(authorized_role)
where docstatus = 1 and item_code = %s if authorized_users and frappe.session.user not in authorized_users:
and warehouse = %s""", (self.item_code, self.warehouse))[0][0] last_transaction_time = frappe.db.sql("""
select MAX(timestamp(posting_date, posting_time)) as posting_time
from `tabStock Ledger Entry`
where docstatus = 1 and item_code = %s
and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time): if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code), msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
frappe.bold(self.warehouse), frappe.bold(last_transaction_time)) frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
msg += "<br><br>" + _("Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.").format( msg += "<br><br>" + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format(
frappe.bold(self.item_code), frappe.bold(self.warehouse)) frappe.bold(self.item_code), frappe.bold(self.warehouse))
msg += "<br><br>" + _("Please remove this item and try to submit again or update the posting time.") msg += "<br><br>" + _("Please contact any of the following users to {} this transaction.")
frappe.throw(msg, title=_("Backdated Stock Entry")) msg += "<br>" + "<br>".join(authorized_users)
frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
def validate_future_posting(self):
if date_diff(self.posting_date, getdate()) > 0:
msg = _("Posting future stock transactions are not allowed due to Immutable Ledger")
frappe.throw(msg, title=_("Future Posting Not Allowed"))
def on_doctype_update(): def on_doctype_update():
if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'): if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'):

View File

@ -5,8 +5,397 @@ from __future__ import unicode_literals
import frappe import frappe
import unittest import unittest
from frappe.utils import today, add_days
# test_records = frappe.get_test_records('Stock Ledger Entry') from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
import create_stock_reconciliation
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction
class TestStockLedgerEntry(unittest.TestCase): class TestStockLedgerEntry(unittest.TestCase):
pass def setUp(self):
items = create_items()
# delete SLE and BINs for all items
frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
def test_item_cost_reposting(self):
company = "_Test Company"
# _Test Item for Reposting at Stores warehouse on 10-04-2020: Qty = 50, Rate = 100
create_stock_reconciliation(
item_code="_Test Item for Reposting",
warehouse="Stores - _TC",
qty=50,
rate=100,
company=company,
expense_account = "Stock Adjustment - _TC",
posting_date='2020-04-10',
posting_time='14:00'
)
# _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200
create_stock_reconciliation(
item_code="_Test Item for Reposting",
warehouse="Finished Goods - _TC",
qty=10,
rate=200,
company=company,
expense_account = "Stock Adjustment - _TC",
posting_date='2020-04-20',
posting_time='14:00'
)
# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
make_stock_entry(
item_code="_Test Item for Reposting",
source="Stores - _TC",
target="Finished Goods - _TC",
company=company,
qty=10,
expense_account="Stock Adjustment - _TC",
posting_date='2020-04-30',
posting_time='14:00'
)
target_wh_sle = get_previous_sle({
"item_code": "_Test Item for Reposting",
"warehouse": "Finished Goods - _TC",
"posting_date": '2020-04-30',
"posting_time": '14:00'
})
self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
# Repack entry on 5-5-2020
repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
finished_item_sle = get_previous_sle({
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
"posting_date": '2020-05-05',
"posting_time": '14:00'
})
self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
create_stock_reconciliation(
item_code="_Test Item for Reposting",
warehouse="Stores - _TC",
qty=50,
rate=150,
company=company,
expense_account = "Stock Adjustment - _TC",
posting_date='2020-04-12',
posting_time='14:00'
)
# Check valuation rate of finished goods warehouse after back-dated entry at Stores
target_wh_sle = get_previous_sle({
"item_code": "_Test Item for Reposting",
"warehouse": "Finished Goods - _TC",
"posting_date": '2020-04-30',
"posting_time": '14:00'
})
self.assertEqual(target_wh_sle.get("incoming_rate"), 150)
self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
# Check valuation rate of repacked item after back-dated entry at Stores
finished_item_sle = get_previous_sle({
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
"posting_date": '2020-05-05',
"posting_time": '14:00'
})
self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
# Check updated rate in Repack entry
repack.reload()
self.assertEqual(repack.items[0].get("basic_rate"), 150)
self.assertEqual(repack.items[1].get("basic_rate"), 750)
def test_purchase_return_valuation_reposting(self):
pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10',
warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100)
return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2)
# check sle
outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
self.assertEqual(outgoing_rate, 100)
self.assertEqual(stock_value_difference, -200)
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
self.assertEqual(outgoing_rate, 110)
self.assertEqual(stock_value_difference, -220)
def test_sales_return_valuation_reposting(self):
company = "_Test Company"
item_code="_Test Item for Reposting"
# Purchase Return: Qty = 5, Rate = 100
pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100)
#Delivery Note: Qty = 5, Rate = 150
dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC",
company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
# check outgoing_rate for DN
outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
"voucher_no": dn.name}, "stock_value_difference") / 5)
self.assertEqual(dn.items[0].incoming_rate, 100)
self.assertEqual(outgoing_rate, 100)
# Return Entry: Qty = -2, Rate = 150
return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150,
company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
# check incoming rate for Return entry
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
["incoming_rate", "stock_value_difference"])
self.assertEqual(return_dn.items[0].incoming_rate, 100)
self.assertEqual(incoming_rate, 100)
self.assertEqual(stock_value_difference, 200)
#-------------------------------
# Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
# check outgoing_rate for DN after reposting
outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
"voucher_no": dn.name}, "stock_value_difference") / 5)
self.assertEqual(outgoing_rate, 110)
dn.reload()
self.assertEqual(dn.items[0].incoming_rate, 110)
# check incoming rate for Return entry after reposting
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
["incoming_rate", "stock_value_difference"])
self.assertEqual(incoming_rate, 110)
self.assertEqual(stock_value_difference, 220)
return_dn.reload()
self.assertEqual(return_dn.items[0].incoming_rate, 110)
# Cleanup data
return_dn.cancel()
dn.cancel()
lcv.cancel()
pr.cancel()
def test_reposting_of_sales_return_for_packed_item(self):
company = "_Test Company"
packed_item_code="_Test Item for Reposting"
bundled_item = "_Test Bundled Item for Reposting"
create_product_bundle_item(bundled_item, [[packed_item_code, 4]])
# Purchase Return: Qty = 50, Rate = 100
pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100)
#Delivery Note: Qty = 5, Rate = 150
dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC",
company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
# check outgoing_rate for DN
outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
"voucher_no": dn.name}, "stock_value_difference") / 20)
self.assertEqual(dn.packed_items[0].incoming_rate, 100)
self.assertEqual(outgoing_rate, 100)
# Return Entry: Qty = -2, Rate = 150
return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
# check incoming rate for Return entry
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
["incoming_rate", "stock_value_difference"])
self.assertEqual(return_dn.packed_items[0].incoming_rate, 100)
self.assertEqual(incoming_rate, 100)
self.assertEqual(stock_value_difference, 800)
#-------------------------------
# Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
# check outgoing_rate for DN after reposting
outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
"voucher_no": dn.name}, "stock_value_difference") / 20)
self.assertEqual(outgoing_rate, 101)
dn.reload()
self.assertEqual(dn.packed_items[0].incoming_rate, 101)
# check incoming rate for Return entry after reposting
incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
{"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
["incoming_rate", "stock_value_difference"])
self.assertEqual(incoming_rate, 101)
self.assertEqual(stock_value_difference, 808)
return_dn.reload()
self.assertEqual(return_dn.packed_items[0].incoming_rate, 101)
# Cleanup data
return_dn.cancel()
dn.cancel()
lcv.cancel()
pr.cancel()
def test_sub_contracted_item_costing(self):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
company = "_Test Company"
rm_item_code="_Test Item for Reposting"
subcontracted_item = "_Test Subcontracted Item for Reposting"
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR")
# Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100)
# Purchase Receipt for subcontracted item
pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20',
warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC",
item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes")
self.assertEqual(pr1.items[0].valuation_rate, 120)
# Update raw material's valuation via LCV, Additional cost = 50
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
pr1.reload()
self.assertEqual(pr1.items[0].valuation_rate, 125)
# check outgoing_rate for DN after reposting
incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
"voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate")
self.assertEqual(incoming_rate, 125)
# cleanup data
pr1.cancel()
lcv.cancel()
pr.cancel()
def test_back_dated_entry_not_allowed(self):
# Back dated stock transactions are only allowed to stock managers
frappe.db.set_value("Stock Settings", None,
"role_allowed_to_create_edit_back_dated_transactions", "Stock Manager")
# Set User with Stock User role but not Stock Manager
frappe.set_user("test@example.com")
user = frappe.get_doc("User", "test@example.com")
user.add_roles("Stock User")
user.remove_roles("Stock Manager")
stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
posting_date=add_days(today(), -1), do_not_submit=True)
# Block back-dated entry
self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
user.add_roles("Stock Manager")
# Back dated entry allowed to Stock Manager
back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
posting_date=add_days(today(), -1))
back_dated_se_2.cancel()
stock_entry_on_today.cancel()
frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
frappe.set_user("Administrator")
def create_repack_entry(**args):
args = frappe._dict(args)
repack = frappe.new_doc("Stock Entry")
repack.stock_entry_type = "Repack"
repack.company = args.company or "_Test Company"
repack.posting_date = args.posting_date
repack.set_posting_time = 1
repack.append("items", {
"item_code": "_Test Item for Reposting",
"s_warehouse": "Stores - _TC",
"qty": 5,
"conversion_factor": 1,
"expense_account": "Stock Adjustment - _TC",
"cost_center": "Main - _TC"
})
repack.append("items", {
"item_code": "_Test Finished Item for Reposting",
"t_warehouse": "Finished Goods - _TC",
"qty": 1,
"conversion_factor": 1,
"expense_account": "Stock Adjustment - _TC",
"cost_center": "Main - _TC"
})
repack.append("additional_costs", {
"expense_account": "Freight and Forwarding Charges - _TC",
"description": "transport cost",
"amount": 40
})
repack.save()
repack.submit()
return repack
def create_product_bundle_item(new_item_code, packed_items):
if not frappe.db.exists("Product Bundle", new_item_code):
item = frappe.new_doc("Product Bundle")
item.new_item_code = new_item_code
for d in packed_items:
item.append("items", {
"item_code": d[0],
"qty": d[1]
})
item.save()
def create_items():
items = ["_Test Item for Reposting", "_Test Finished Item for Reposting",
"_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"]
for d in items:
properties = {"valuation_method": "FIFO"}
if d == "_Test Bundled Item for Reposting":
properties.update({"is_stock_item": 0})
elif d == "_Test Subcontracted Item for Reposting":
properties.update({"is_sub_contracted_item": 1})
make_item(d, properties=properties)
return items

View File

@ -37,14 +37,16 @@ class StockReconciliation(StockController):
def on_submit(self): def on_submit(self):
self.update_stock_ledger() self.update_stock_ledger()
self.make_gl_entries() self.make_gl_entries()
self.repost_future_sle_and_gle()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items") update_serial_nos_after_submit(self, "items")
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_sle_on_cancel() self.make_sle_on_cancel()
self.make_gl_entries_on_cancel() self.make_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
def remove_items_with_no_change(self): def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed""" """Remove items if qty or rate is not changed"""

View File

@ -8,12 +8,11 @@ from __future__ import unicode_literals
import frappe, unittest import frappe, unittest
from frappe.utils import flt, nowdate, nowtime from frappe.utils import flt, nowdate, nowtime
from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class TestStockReconciliation(unittest.TestCase): class TestStockReconciliation(unittest.TestCase):
@ -29,16 +28,17 @@ class TestStockReconciliation(unittest.TestCase):
self._test_reco_sle_gle("Moving Average") self._test_reco_sle_gle("Moving Average")
def _test_reco_sle_gle(self, valuation_method): def _test_reco_sle_gle(self, valuation_method):
insert_existing_sle(warehouse='Stores - TCP1') se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1')
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company') company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
# [[qty, valuation_rate, posting_date, # [[qty, valuation_rate, posting_date,
# posting_time, expected_stock_value, bin_qty, bin_valuation]] # posting_time, expected_stock_value, bin_qty, bin_valuation]]
input_data = [ input_data = [
[50, 1000], [50, 1000, "2012-12-26", "12:00"],
[25, 900], [25, 900, "2012-12-26", "12:00"],
["", 1000], ["", 1000, "2012-12-20", "12:05"],
[20, ""], [20, "", "2012-12-26", "12:05"],
[0, ""] [0, "", "2012-12-31", "12:10"]
] ]
for d in input_data: for d in input_data:
@ -47,13 +47,13 @@ class TestStockReconciliation(unittest.TestCase):
last_sle = get_previous_sle({ last_sle = get_previous_sle({
"item_code": "_Test Item", "item_code": "_Test Item",
"warehouse": "Stores - TCP1", "warehouse": "Stores - TCP1",
"posting_date": nowdate(), "posting_date": d[2],
"posting_time": nowtime() "posting_time": d[3]
}) })
# submit stock reconciliation # submit stock reconciliation
stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1], stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1],
posting_date=nowdate(), posting_time=nowtime(), warehouse="Stores - TCP1", posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1",
company=company, expense_account = "Stock Adjustment - TCP1") company=company, expense_account = "Stock Adjustment - TCP1")
# check stock value # check stock value
@ -81,10 +81,15 @@ class TestStockReconciliation(unittest.TestCase):
stock_reco.cancel() stock_reco.cancel()
se3.cancel()
se2.cancel()
se1.cancel()
def test_get_items(self): def test_get_items(self):
create_warehouse("_Test Warehouse Group 1", {"is_group": 1}) create_warehouse("_Test Warehouse Group 1",
{"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"})
create_warehouse("_Test Warehouse Ledger 1", create_warehouse("_Test Warehouse Ledger 1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"}) {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"})
create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100, create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100,
warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100) warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100)
@ -95,8 +100,6 @@ class TestStockReconciliation(unittest.TestCase):
[items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]]) [items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]])
def test_stock_reco_for_serialized_item(self): def test_stock_reco_for_serialized_item(self):
set_perpetual_inventory()
to_delete_records = [] to_delete_records = []
to_delete_serial_nos = [] to_delete_serial_nos = []
@ -148,8 +151,6 @@ class TestStockReconciliation(unittest.TestCase):
stock_doc.cancel() stock_doc.cancel()
def test_stock_reco_for_batch_item(self): def test_stock_reco_for_batch_item(self):
set_perpetual_inventory()
to_delete_records = [] to_delete_records = []
to_delete_serial_nos = [] to_delete_serial_nos = []
@ -196,15 +197,17 @@ class TestStockReconciliation(unittest.TestCase):
def insert_existing_sle(warehouse): def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item",
target=warehouse, qty=10, basic_rate=700) target=warehouse, qty=10, basic_rate=700)
make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item",
source=warehouse, qty=15) source=warehouse, qty=15)
make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item", se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item",
target=warehouse, qty=15, basic_rate=1200) target=warehouse, qty=15, basic_rate=1200)
return se1, se2, se3
def create_batch_or_serial_no_items(): def create_batch_or_serial_no_items():
create_warehouse("_Test Warehouse for Stock Reco1", create_warehouse("_Test Warehouse for Stock Reco1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"}) {"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
@ -256,6 +259,10 @@ def create_stock_reconciliation(**args):
return sr return sr
def set_valuation_method(item_code, valuation_method): def set_valuation_method(item_code, valuation_method):
existing_valuation_method = get_valuation_method(item_code)
if valuation_method == existing_valuation_method:
return
frappe.db.set_value("Item", item_code, "valuation_method", valuation_method) frappe.db.set_value("Item", item_code, "valuation_method", valuation_method)
for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]): for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]):

View File

@ -28,7 +28,9 @@
"inter_warehouse_transfer_settings_section", "inter_warehouse_transfer_settings_section",
"allow_from_dn", "allow_from_dn",
"allow_from_pr", "allow_from_pr",
"freeze_stock_entries", "control_historical_stock_transactions_section",
"role_allowed_to_create_edit_back_dated_transactions",
"column_break_26",
"stock_frozen_upto", "stock_frozen_upto",
"stock_frozen_upto_days", "stock_frozen_upto_days",
"stock_auth_role", "stock_auth_role",
@ -156,21 +158,20 @@
"label": "Notify by Email on Creation of Automatic Material Request" "label": "Notify by Email on Creation of Automatic Material Request"
}, },
{ {
"fieldname": "freeze_stock_entries", "description": "No stock transactions can be created or modified before this date.",
"fieldtype": "Section Break",
"label": "Freeze Stock Entries"
},
{
"fieldname": "stock_frozen_upto", "fieldname": "stock_frozen_upto",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Stock Frozen Upto" "label": "Stock Frozen Upto"
}, },
{ {
"description": "Stock transactions that are older than the mentioned days cannot be modified.",
"fieldname": "stock_frozen_upto_days", "fieldname": "stock_frozen_upto_days",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Freeze Stocks Older Than (Days)" "label": "Freeze Stocks Older Than (Days)"
}, },
{ {
"depends_on": "eval:(doc.stock_frozen_upto || doc.stock_frozen_upto_days)",
"description": "The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.",
"fieldname": "stock_auth_role", "fieldname": "stock_auth_role",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Role Allowed to Edit Frozen Stock", "label": "Role Allowed to Edit Frozen Stock",
@ -210,6 +211,22 @@
"fieldname": "allow_from_pr", "fieldname": "allow_from_pr",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice" "label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice"
},
{
"description": "If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",
"fieldname": "role_allowed_to_create_edit_back_dated_transactions",
"fieldtype": "Link",
"label": "Role Allowed to Create/Edit Back-dated Transactions",
"options": "User"
},
{
"fieldname": "column_break_26",
"fieldtype": "Column Break"
},
{
"fieldname": "control_historical_stock_transactions_section",
"fieldtype": "Section Break",
"label": "Control Historical Stock Transactions"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@ -217,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 15:26:54.225608", "modified": "2020-11-23 22:26:54.225608",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@ -10,13 +10,10 @@ from frappe.test_runner import make_test_records
import erpnext import erpnext
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext import set_perpetual_inventory
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
test_records = frappe.get_test_records('Warehouse') test_records = frappe.get_test_records('Warehouse')
class TestWarehouse(unittest.TestCase): class TestWarehouse(unittest.TestCase):
def setUp(self): def setUp(self):
if not frappe.get_value('Item', '_Test Item'): if not frappe.get_value('Item', '_Test Item'):
@ -37,63 +34,63 @@ class TestWarehouse(unittest.TestCase):
self.assertEqual(child_warehouse.is_group, 0) self.assertEqual(child_warehouse.is_group, 0)
def test_warehouse_renaming(self): def test_warehouse_renaming(self):
set_perpetual_inventory(1) create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
create_warehouse("Test Warehouse for Renaming 1") account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
account = get_inventory_account("_Test Company", "Test Warehouse for Renaming 1 - _TC")
self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account})) self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
# Rename with abbr # Rename with abbr
if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - _TC"): if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC") frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - _TC", "Test Warehouse for Renaming 2 - _TC") frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse", self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Renaming 1 - _TC"})) filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Rename without abbr # Rename without abbr
if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - _TC"): if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC") frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC", "Test Warehouse for Renaming 3") frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
self.assertTrue(frappe.db.get_value("Warehouse", self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Renaming 1 - _TC"})) filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Another rename with multiple dashes # Another rename with multiple dashes
if frappe.db.exists("Warehouse", "Test - Warehouse - Company - _TC"): if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
frappe.delete_doc("Warehouse", "Test - Warehouse - Company - _TC") frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC", "Test - Warehouse - Company") frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
def test_warehouse_merging(self): def test_warehouse_merging(self):
set_perpetual_inventory(1) company = "_Test Company with perpetual inventory"
create_warehouse("Test Warehouse for Merging 1", company=company,
properties={"parent_warehouse": "All Warehouses - TCP1"})
create_warehouse("Test Warehouse for Merging 2", company=company,
properties={"parent_warehouse": "All Warehouses - TCP1"})
create_warehouse("Test Warehouse for Merging 1") make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
create_warehouse("Test Warehouse for Merging 2") qty=1, rate=100, company=company)
make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - _TC", qty=1, rate=100, company=company)
qty=1, rate=100)
make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - _TC",
qty=1, rate=100)
existing_bin_qty = ( existing_bin_qty = (
cint(frappe.db.get_value("Bin", cint(frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - _TC"}, "actual_qty")) {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
+ cint(frappe.db.get_value("Bin", + cint(frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty")) {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
) )
frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - _TC", frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
"Test Warehouse for Merging 2 - _TC", merge=True) "Test Warehouse for Merging 2 - TCP1", merge=True)
self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - _TC")) self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
bin_qty = frappe.db.get_value("Bin", bin_qty = frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty") {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
self.assertEqual(bin_qty, existing_bin_qty) self.assertEqual(bin_qty, existing_bin_qty)
self.assertTrue(frappe.db.get_value("Warehouse", self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Merging 2 - _TC"})) filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
def create_warehouse(warehouse_name, properties=None, company=None): def create_warehouse(warehouse_name, properties=None, company=None):
if not company: if not company:

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