Merge branch 'develop' into partially-submit-drop-ship-items-issue

This commit is contained in:
Jannat Patel 2020-12-24 13:44:42 +05:30 committed by GitHub
commit 29dd7e5fc4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
118 changed files with 3429 additions and 1399 deletions

View File

@ -21,8 +21,8 @@ def docs_link_exists(body):
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
_, org, repo, _type, ref = parsed_url.path.split('/')
if org == "frappe" and repo in docs_repos:
parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True

View File

@ -910,98 +910,8 @@
},
"is_group": 1
},
"Passiva": {
"Passiva - Verbindlichkeiten": {
"root_type": "Liability",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_type": "Equity",
"account_number": "2900"
},
"Ausstehende Einlagen auf das gezeichnete Kapital": {
"account_number": "2910",
"is_group": 1
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
},
"B - R\u00fcckstellungen": {
"is_group": 1,
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
@ -1618,6 +1528,143 @@
},
"is_group": 1
},
"Passiva - Eigenkapital": {
"root_type": "Equity",
"A - Eigenkapital": {
"account_type": "Equity",
"is_group": 1,
"I - Gezeichnetes Kapital": {
"account_type": "Equity",
"is_group": 1,
"Gezeichnetes Kapital": {
"account_number": "2900",
"account_type": "Equity"
},
"Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": {
"account_number": "2901"
},
"Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": {
"account_number": "2902"
},
"Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": {
"account_number": "2903"
},
"R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2906"
},
"Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
"account_number": "2907"
},
"Kapitalerh\u00f6hung aus Gesellschaftsmitteln": {
"account_number": "2908"
},
"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": {
"account_number": "2910"
}
},
"II - Kapitalr\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Kapitalr\u00fccklage": {
"account_number": "2920"
},
"Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": {
"account_number": "2925"
},
"Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": {
"account_number": "2926"
},
"Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": {
"account_number": "2927"
},
"Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": {
"account_number": "2928"
},
"Nachschusskapital (Gegenkonto 1299)": {
"account_number": "2929"
}
},
"III - Gewinnr\u00fccklagen": {
"account_type": "Equity",
"1 - gesetzliche R\u00fccklage": {
"account_type": "Equity",
"is_group": 1,
"Gesetzliche R\u00fccklage": {
"account_number": "2930"
}
},
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_type": "Equity",
"is_group": 1,
"R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
"account_number": "2935"
}
},
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
"account_number": "2950"
}
},
"4 - andere Gewinnr\u00fccklagen": {
"account_type": "Equity",
"is_group": 1,
"Andere Gewinnr\u00fccklagen": {
"account_number": "2960"
},
"Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": {
"account_number": "2961"
},
"Eigenkapitalanteil von Wertaufholungen": {
"account_number": "2962"
},
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
"is_group": 1,
"Gewinnr\u00fccklagen (BilMoG)": {
"account_number": "2963"
},
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
"account_number": "2964"
},
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
"account_number": "2965"
},
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
"account_number": "2966"
}
},
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
"account_number": "2967"
},
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2968"
},
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
"account_number": "2969"
}
},
"is_group": 1
},
"IV - Gewinnvortrag/Verlustvortrag": {
"account_type": "Equity",
"is_group": 1,
"Gewinnvortrag vor Verwendung": {
"account_number": "2970"
},
"Verlustvortrag vor Verwendung": {
"account_number": "2978"
}
},
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
"account_type": "Equity",
"is_group": 1
},
"Einlagen stiller Gesellschafter": {
"account_number": "9295"
}
}
},
"1 - Umsatzerl\u00f6se": {
"root_type": "Income",
"is_group": 1,

View File

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

View File

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

View File

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

View File

@ -75,54 +75,40 @@ class TestJournalEntry(unittest.TestCase):
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
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)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
def test_jv_against_stock_account(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
set_perpetual_inventory()
company = "_Test Company with perpetual inventory"
stock_account = get_inventory_account(company)
jv = frappe.copy_doc({
"cheque_date": nowdate(),
"cheque_no": "33",
"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({
"account": get_inventory_account('_Test Company with perpetual inventory'),
"company": "_Test Company with perpetual inventory",
"party_type": None,
"party": None
jv = frappe.new_doc("Journal Entry")
jv.company = company
jv.posting_date = nowdate()
jv.append("accounts", {
"account": stock_account,
"cost_center": "Main - TCP1",
"debit_in_account_currency": 100
})
jv.append("accounts", {
"account": "Stock Adjustment - TCP1",
"credit_in_account_currency": 100,
"cost_center": "Main - TCP1",
})
jv.insert()
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
jv.cancel()
set_perpetual_inventory(0)
from erpnext.accounts.utils import get_stock_and_account_balance
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
if account_bal == stock_bal:
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
frappe.db.rollback()
else:
jv.submit()
jv.cancel()
def test_multi_currency(self):
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 erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
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):
@classmethod
def setUpClass(self):
set_perpetual_inventory(0)
# create relevant item, customer, loyalty program, etc
create_records()

View File

@ -15,6 +15,16 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
return (doc.qty<=doc.received_qty) ? "green" : "orange";
});
}
this.frm.set_query("unrealized_profit_loss_account", function() {
return {
filters: {
company: doc.company,
is_group: 0,
root_type: "Liability",
}
};
});
},
onload: function() {
this._super();

View File

@ -1,6 +1,5 @@
{
"actions": [],
"allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@ -127,6 +126,7 @@
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
"adjust_advance_taxes",
"get_advances",
"advances",
"payment_schedule_section",
@ -152,9 +152,11 @@
"is_opening",
"against_expense_account",
"column_break_63",
"unrealized_profit_loss_account",
"status",
"inter_company_invoice_reference",
"is_internal_supplier",
"represents_company",
"remarks",
"subscription_section",
"from_date",
@ -1223,7 +1225,7 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled",
"options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1
},
{
@ -1330,13 +1332,37 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
"description": "Taxes paid while advance payment will be adjusted against this invoice",
"fieldname": "adjust_advance_taxes",
"fieldtype": "Check",
"label": "Adjust Advance Taxes"
},
{
"depends_on": "eval:doc.is_internal_supplier",
"description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account",
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
},
{
"depends_on": "eval:doc.is_internal_supplier",
"description": "Company which internal supplier represents",
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"label": "Represents Company",
"options": "Company"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2020-10-30 13:57:18.266978",
"modified": "2020-12-11 12:46:12.796378",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -206,8 +206,8 @@ class PurchaseInvoice(BuyingController):
["Purchase Receipt", "purchase_receipt", "pr_detail"]
])
def validate_warehouse(self):
if self.update_stock:
def validate_warehouse(self, for_validate=True):
if self.update_stock and for_validate:
for d in self.get('items'):
if not d.warehouse:
frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}").
@ -233,7 +233,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock:
self.validate_item_code()
self.validate_warehouse()
self.validate_warehouse(for_validate)
if auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@ -410,10 +410,13 @@ class PurchaseInvoice(BuyingController):
# this sequence because outstanding may get -negative
self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_project()
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:
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"
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:
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)
if self.auto_accounting_for_stock:
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:
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
gl_entries = []
@ -449,15 +454,15 @@ class PurchaseInvoice(BuyingController):
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
self.make_payment_gl_entries(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
return gl_entries
def check_asset_cwip_enabled(self):
@ -474,31 +479,30 @@ class PurchaseInvoice(BuyingController):
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
if grand_total:
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center
}, self.party_account_currency, item=self)
)
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
"party_type": "Supplier",
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
"project": self.project,
"cost_center": self.cost_center
}, self.party_account_currency, item=self)
)
def make_item_gl_entries(self, gl_entries):
# item gl entries
stock_items = self.get_stock_items()
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@ -526,7 +530,6 @@ class PurchaseInvoice(BuyingController):
item, voucher_wise_stock_value, account_currency)
if item.from_warehouse:
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[item.warehouse]['account'],
"against": warehouse_account[item.from_warehouse]["account"],
@ -546,16 +549,18 @@ class PurchaseInvoice(BuyingController):
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
}, warehouse_account[item.from_warehouse]["account_currency"], item=item))
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
# Do not book expense for transfer within same company transfer
if not self.is_internal_transfer():
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"against": self.supplier,
"debit": flt(item.base_net_amount, item.precision("base_net_amount")),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
else:
gl_entries.append(
@ -832,7 +837,8 @@ class PurchaseInvoice(BuyingController):
}, account_currency, item=tax)
)
# accumulate valuation tax
if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount):
if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \
and not self.is_internal_transfer():
if self.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
valuation_tax.setdefault(tax.name, 0)
@ -876,8 +882,19 @@ class PurchaseInvoice(BuyingController):
"against": self.supplier,
"credit": valuation_tax[tax.name],
"remarks": self.remarks or "Accounting Entry for Stock"
}, item=tax)
)
}, item=tax))
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
gl_entries.append(
self.get_gl_dict({
"account": self.unrealized_profit_loss_account,
"against": self.supplier,
"credit": flt(self.total_taxes_and_charges),
"credit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center
}, account_currency, item=self))
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
@ -982,11 +999,15 @@ class PurchaseInvoice(BuyingController):
self.delete_auto_created_batches()
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
self.update_project()
frappe.db.set(self, 'status', 'Cancelled')
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):
project_list = []
@ -1095,7 +1116,9 @@ class PurchaseInvoice(BuyingController):
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
if outstanding_amount > 0 and due_date < nowdate:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate:
self.status = "Unpaid"

View File

@ -4,23 +4,25 @@
// render
frappe.listview_settings['Purchase Invoice'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
"currency", "is_return", "release_date", "on_hold"],
"currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
get_indicator: function(doc) {
if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
} else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
} else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
if(cint(doc.on_hold) && !doc.release_date) {
return [__("On Hold"), "darkgrey"];
} else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
} else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
return [__("Temporarily on Hold"), "darkgrey"];
} else if(frappe.datetime.get_diff(doc.due_date) < 0) {
} else if (frappe.datetime.get_diff(doc.due_date) < 0) {
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
} else {
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
}
} else if(cint(doc.is_return)) {
} else if (cint(doc.is_return)) {
return [__("Return"), "darkgrey", "is_return,=,Yes"];
} else if(flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
} else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
} else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
return [__("Paid"), "green", "outstanding_amount,=,0"];
}
}

View File

@ -9,8 +9,7 @@ import frappe.model
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import cint, flt, today, nowdate, add_days, getdate
import frappe.defaults
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
test_records as pr_test_records, make_purchase_receipt, get_taxes
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency
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):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
wrapper = frappe.copy_doc(test_records[0])
set_perpetual_inventory(0, wrapper.company)
self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company)))
wrapper.insert()
wrapper.submit()
wrapper.load_from_db()
dl = wrapper
pi = frappe.copy_doc(test_records[0])
self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company)))
pi.insert()
pi.submit()
expected_gl_entries = {
"_Test Payable - _TC": [0, 1512.0],
@ -54,12 +50,16 @@ class TestPurchaseInvoice(unittest.TestCase):
"Round Off - _TC": [0, 0.3]
}
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:
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
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.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,)
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")
for d in pi.items:
@ -247,17 +245,11 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
def test_gl_entries_with_aia_for_non_stock_items(self):
pi = frappe.copy_doc(test_records[1])
set_perpetual_inventory(1, pi.company)
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
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()
def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
self.assertTrue(pi.status, "Unpaid")
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)
self.assertTrue(gl_entries)
expected_values = sorted([
["_Test Payable - _TC", 0, 620],
["_Test Account Cost for Goods Sold - _TC", 500.0, 0],
["_Test Account VAT - _TC", 120.0, 0],
])
expected_values = [
["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
["Creditors - TCP1", 0, 250]
]
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
set_perpetual_inventory(0, pi.company)
def test_purchase_invoice_calculation(self):
pi = frappe.copy_doc(test_records[0])
@ -457,12 +447,13 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.cancel()
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
def test_return_purchase_invoice(self):
set_perpetual_inventory()
def test_return_purchase_invoice_with_perpetual_inventory(self):
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)
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
# check gl entries for return
@ -473,19 +464,15 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = {
"Creditors - _TC": [100.0, 0.0],
"Stock Received But Not Billed - _TC": [0.0, 100.0],
"Creditors - TCP1": [100.0, 0.0],
"Stock Received But Not Billed - TCP1": [0.0, 100.0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
set_perpetual_inventory(0)
def test_multi_currency_gle(self):
set_perpetual_inventory(0)
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50)
@ -640,10 +627,9 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2)
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):
set_perpetual_inventory(0)
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_warehouse = "_Test Rejected Warehouse - _TC")

View File

@ -580,6 +580,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
frm.set_query("unrealized_profit_loss_account", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
root_type: "Liability",
}
};
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Sales Return',

View File

@ -1,6 +1,5 @@
{
"actions": [],
"allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:05",
@ -158,6 +157,7 @@
"more_information",
"inter_company_invoice_reference",
"is_internal_customer",
"represents_company",
"customer_group",
"campaign",
"is_discounted",
@ -171,6 +171,7 @@
"c_form_applicable",
"c_form_no",
"column_break8",
"unrealized_profit_loss_account",
"remarks",
"sales_team_section_break",
"sales_partner",
@ -1655,7 +1656,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
"read_only": 1
},
@ -1950,13 +1951,31 @@
"fieldtype": "Data",
"label": "Company Tax ID",
"read_only": 1
},
{
"depends_on": "eval:doc.is_internal_customer",
"description": "Unrealized Profit / Loss account for intra-company transfers",
"fieldname": "unrealized_profit_loss_account",
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
},
{
"depends_on": "eval:doc.is_internal_customer",
"description": "Company which internal customer represents",
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"label": "Represents Company",
"options": "Company",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
"links": [],
"modified": "2020-10-30 13:57:45.086303",
"modified": "2020-12-11 12:48:31.769958",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -179,6 +179,9 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve
self.make_gl_entries()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
@ -258,6 +261,10 @@ class SalesInvoice(SellingController):
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
if self.update_stock == 1:
self.repost_future_sle_and_gle()
frappe.db.set(self, 'status', 'Cancelled')
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:
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):
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:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
def make_gl_entries(self, gl_entries=None):
from erpnext.accounts.general_ledger import make_reverse_gl_entries
def make_gl_entries(self, gl_entries=None, from_repost=False):
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not gl_entries:
gl_entries = self.get_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
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
cint(self.redeem_loyalty_points)) else "Yes"
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:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@ -758,6 +763,7 @@ class SalesInvoice(SellingController):
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
@ -777,7 +783,7 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
if grand_total:
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
@ -816,6 +822,18 @@ class SalesInvoice(SellingController):
}, account_currency, item=tax)
)
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
gl_entries.append(
self.get_gl_dict({
"account": self.unrealized_profit_loss_account,
"against": self.customer,
"debit": flt(self.total_taxes_and_charges),
"debit_in_account_currency": flt(self.base_total_taxes_and_charges),
"cost_center": self.cost_center
}, account_currency, item=self))
def make_item_gl_entries(self, gl_entries):
# income account gl entries
for item in self.get("items"):
@ -838,22 +856,24 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", self.posting_date)
asset.set_status("Sold" if self.docstatus==1 else None)
else:
income_account = (item.income_account
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
# Do not book income for transfer within same company
if not self.is_internal_transfer():
income_account = (item.income_account
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict({
"account": income_account,
"against": self.customer,
"credit": flt(item.base_net_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
if account_currency==self.company_currency
else flt(item.net_amount, item.precision("net_amount"))),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict({
"account": income_account,
"against": self.customer,
"credit": flt(item.base_net_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
if account_currency==self.company_currency
else flt(item.net_amount, item.precision("net_amount"))),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
# expense account gl entries
if cint(self.update_stock) and \
@ -1265,7 +1285,9 @@ class SalesInvoice(SellingController):
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
@ -1530,9 +1552,13 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
source_document_warehouse_field = 'target_warehouse'
target_document_warehouse_field = 'from_warehouse'
else:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
source_document_warehouse_field = 'from_warehouse'
target_document_warehouse_field = 'target_warehouse'
validate_inter_company_transaction(source_doc, doctype)
details = get_inter_company_details(source_doc, doctype)
@ -1559,6 +1585,26 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
if currency:
target_doc.currency = currency
item_field_map = {
"doctype": target_doctype + " Item",
"field_no_map": [
"income_account",
"expense_account",
"cost_center",
"warehouse"
]
}
if source_doc.get('update_stock'):
item_field_map.update({
'field_map': {
source_document_warehouse_field: target_document_warehouse_field,
'batch_no': 'batch_no',
'serial_no': 'serial_no'
}
})
doclist = get_mapped_doc(doctype, source_name, {
doctype: {
"doctype": target_doctype,
@ -1567,15 +1613,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"taxes_and_charges"
]
},
doctype +" Item": {
"doctype": target_doctype + " Item",
"field_no_map": [
"income_account",
"expense_account",
"cost_center",
"warehouse"
]
}
doctype +" Item": item_field_map
}, target_doc, set_missing_values)

View File

@ -14,8 +14,8 @@ frappe.listview_settings['Sales Invoice'] = {
"Credit Note Issued": "darkgrey",
"Unpaid and Discounted": "orange",
"Overdue and Discounted": "red",
"Overdue": "red"
"Overdue": "red",
"Internal Transfer": "darkgrey"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
},

View File

@ -17,7 +17,8 @@
"description": "138-CMS Shoe",
"doctype": "Sales Invoice Item",
"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",
"parentfield": "items",
"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.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.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
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):
si = frappe.copy_doc(test_records[1])
set_perpetual_inventory(0, si.company)
si.insert()
si.submit()
@ -815,7 +813,6 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
def test_pos_si_without_payment(self):
set_perpetual_inventory()
make_pos_profile()
pos = copy.deepcopy(test_records[1])
@ -829,9 +826,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
set_perpetual_inventory()
si = frappe.get_doc(test_records[1])
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.get("items")[0].item_code = None
si.insert()
si.submit()
@ -842,24 +838,16 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
[si.debit_to, 630.0, 0.0],
[test_records[1]["items"][0]["income_account"], 0.0, 500.0],
[test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
[test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
["Debtors - TCP1", 100.0, 0.0],
["Sales - TCP1", 0.0, 100.0]
])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
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):
set_perpetual_inventory()
si = frappe.get_doc(test_records[1])
si.get("items")[0].item_code = "_Test Non Stock Item"
si.insert()
si.submit()
si = create_sales_invoice(item="_Test Non Stock Item")
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
@ -867,17 +855,14 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
[si.debit_to, 630.0, 0.0],
[test_records[1]["items"][0]["income_account"], 0.0, 500.0],
[test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
[test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
[si.debit_to, 100.0, 0.0],
[test_records[1]["items"][0]["income_account"], 0.0, 100.0]
])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
set_perpetual_inventory(0)
def _insert_purchase_receipt(self):
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)
def test_multi_currency_gle(self):
set_perpetual_inventory(0)
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50)
@ -1573,7 +1557,7 @@ class TestSalesInvoice(unittest.TestCase):
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
def test_sales_invoice_with_project_link(self):
from erpnext.projects.doctype.project.test_project import make_project
@ -1607,9 +1591,9 @@ class TestSalesInvoice(unittest.TestCase):
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
order by account asc""", sales_invoice.name, as_dict=1)
self.assertTrue(gl_entries)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["project"], gle.project)
@ -1776,11 +1760,70 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
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()
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
# def test_internal_transfer_gl_entry(self):
# ## Create internal transfer account
# account = create_account(account_name="Unrealized Profit",
# parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
# frappe.db.set_value('Company', '_Test Company with perpetual inventory',
# 'unrealized_profit_loss_account', account)
# customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
# "_Test Company with perpetual inventory")
# create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
# "_Test Company with perpetual inventory")
# si = create_sales_invoice(
# company = "_Test Company with perpetual inventory",
# customer = customer,
# debit_to = "Debtors - TCP1",
# warehouse = "Stores - TCP1",
# income_account = "Sales - TCP1",
# expense_account = "Cost of Goods Sold - TCP1",
# cost_center = "Main - TCP1",
# currency = "INR",
# do_not_save = 1
# )
# si.selling_price_list = "_Test Price List Rest of the World"
# si.update_stock = 1
# si.items[0].target_warehouse = 'Work In Progress - TCP1'
# add_taxes(si)
# si.save()
# si.submit()
# target_doc = make_inter_company_transaction("Sales Invoice", si.name)
# target_doc.company = '_Test Company with perpetual inventory'
# target_doc.items[0].warehouse = 'Finished Goods - TCP1'
# add_taxes(target_doc)
# target_doc.save()
# target_doc.submit()
# si_gl_entries = [
# ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
# ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
# ]
# check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
# pi_gl_entries = [
# ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
# ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
# ]
# check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
def test_eway_bill_json(self):
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
@ -1937,14 +1980,19 @@ def create_sales_invoice(**args):
si.append("items", {
"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",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"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,
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _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:
@ -2039,4 +2087,57 @@ def get_taxes_and_charges():
"parentfield": "taxes",
"rate": 2,
"row_id": 1
}]
}]
def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc({
"customer_group": "_Test Customer Group",
"customer_name": customer_name,
"customer_type": "Individual",
"doctype": "Customer",
"territory": "_Test Territory",
"is_internal_customer": 1,
"represents_company": represents_company
})
customer.append("companies", {
"company": allowed_to_interact_with
})
customer.insert()
customer_name = customer.name
else:
customer_name = frappe.db.get_value("Customer", customer_name)
return customer_name
def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
if not frappe.db.exists("Supplier", supplier_name):
supplier = frappe.get_doc({
"supplier_group": "_Test Supplier Group",
"supplier_name": supplier_name,
"doctype": "Supplier",
"is_internal_supplier": 1,
"represents_company": represents_company
})
supplier.append("companies", {
"company": allowed_to_interact_with
})
supplier.insert()
supplier_name = supplier.name
else:
supplier_name = frappe.db.exists("Supplier", supplier_name)
return supplier_name
def add_taxes(doc):
doc.append('taxes', {
'account_head': '_Test Account Excise Duty - TCP1',
"charge_type": "On Net Total",
"cost_center": "Main - TCP1",
"description": "Excise Duty",
"rate": 12
})

View File

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

View File

@ -15,13 +15,13 @@ 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 not cancel:
validate_accounting_period(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
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:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else:
@ -119,8 +119,9 @@ def check_if_in_list(gle, gl_map, dimensions=None):
if same_head:
return e
def save_entries(gl_map, adv_adj, update_outstanding):
validate_cwip_accounts(gl_map)
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost:
validate_cwip_accounts(gl_map)
round_off_debit_credit(gl_map)
@ -128,24 +129,24 @@ def save_entries(gl_map, adv_adj, update_outstanding):
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
for entry in gl_map:
make_entry(entry, adv_adj, update_outstanding)
make_entry(entry, adv_adj, update_outstanding, from_repost)
# check against budget
validate_expense_against_budget(entry)
validate_account_for_perpetual_inventory(gl_map)
if not from_repost:
validate_account_for_perpetual_inventory(gl_map)
def make_entry(args, adv_adj, update_outstanding):
def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle = frappe.new_doc("GL Entry")
gle.update(args)
gle.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost
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()
# check against budget
validate_expense_against_budget(args)
if not from_repost:
validate_expense_against_budget(args)
def validate_account_for_perpetual_inventory(gl_map):
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
@ -161,7 +162,7 @@ def validate_account_for_perpetual_inventory(gl_map):
# 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)
gl_map[0].posting_date, gl_map[0].company)
if gl_map[0].voucher_type=="Journal Entry":
# In case of Journal Entry, there are no corresponding SL entries,
@ -176,8 +177,8 @@ def validate_account_for_perpetual_inventory(gl_map):
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_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses on {3}.").format(
stock_bal, account_bal, frappe.bold(account), gl_map[0].posting_date)
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
@ -185,9 +186,10 @@ def validate_account_for_perpetual_inventory(gl_map):
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) }]
'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),

View File

@ -42,11 +42,13 @@
{% if(filters.show_future_payments) { %}
{% var balance_row = data.slice(-1).pop();
var range1 = report.columns[11].label;
var range2 = report.columns[12].label;
var range3 = report.columns[13].label;
var range4 = report.columns[14].label;
var range5 = report.columns[15].label;
var start = filters.based_on_payment_terms ? 13 : 11;
var range1 = report.columns[start].label;
var range2 = report.columns[start+1].label;
var range3 = report.columns[start+2].label;
var range4 = report.columns[start+3].label;
var range5 = report.columns[start+4].label;
var range6 = report.columns[start+5].label;
%}
{% if(balance_row) { %}
<table class="table table-bordered table-condensed">
@ -70,20 +72,34 @@
<th>{%= __(range3) %}</th>
<th>{%= __(range4) %}</th>
<th>{%= __(range5) %}</th>
<th>{%= __(range6) %}</th>
<th>{%= __("Total") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
<td class="text-right">{%= format_number(balance_row["range1"], null, 2) %}</td>
<td class="text-right">{%= format_currency(balance_row["range2"]) %}</td>
<td class="text-right">{%= format_currency(balance_row["range3"]) %}</td>
<td class="text-right">{%= format_currency(balance_row["range4"]) %}</td>
<td class="text-right">{%= format_currency(balance_row["range5"]) %}</td>
<td class="text-right">
{%= format_number(balance_row["age"], null, 2) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %}
</td>
</td>
</tr>
<td>{%= __("Future Payments") %}</td>
<td></td>
@ -91,6 +107,7 @@
<td></td>
<td></td>
<td></td>
<td></td>
<td class="text-right">
{%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
</td>
@ -101,6 +118,7 @@
<th></th>
<th></th>
<th></th>
<th></th>
<th class="text-right">
{%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th>
</tr>
@ -218,15 +236,15 @@
<td></td>
<td style="text-align: right"><b>{%= __("Total") %}</b></td>
<td style="text-align: right">
{%= format_currency(data[i]["invoiced"], data[0]["currency"] ) %}</td>
{%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %}</td>
{% if(!filters.show_future_payments) { %}
<td style="text-align: right">
{%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %} </td>
{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} </td>
{% } %}
<td style="text-align: right">
{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td>
{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
@ -234,8 +252,8 @@
{%= data[i]["po_no"] %}</td>
{% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[0]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[0]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
{% } else { %}
@ -256,10 +274,10 @@
{% } else { %}
<td><b>{%= __("Total") %}</b></td>
{% } %}
<td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[0]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
</tr>

View File

@ -928,7 +928,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
_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:
_delete_gl_entries(voucher_type, voucher_no)
@ -947,7 +947,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
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),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
@ -964,3 +967,20 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
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

View File

@ -13,8 +13,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
class AssetValueAdjustment(Document):
def validate(self):
self.validate_date()
self.set_difference_amount()
self.set_current_asset_value()
self.set_difference_amount()
def on_submit(self):
self.make_depreciation_entry()
@ -25,7 +25,7 @@ class AssetValueAdjustment(Document):
frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
self.reschedule_depreciations(self.current_asset_value)
def validate_date(self):
asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date')
if getdate(self.date) < getdate(asset_purchase_date):
@ -53,6 +53,7 @@ class AssetValueAdjustment(Document):
je.posting_date = self.date
je.company = self.company
je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount)
je.finance_book = self.finance_book
credit_entry = {
"account": accumulated_depreciation_account,
@ -78,7 +79,7 @@ class AssetValueAdjustment(Document):
debit_entry.update({
dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension')
})
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)

View File

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

View File

@ -49,6 +49,12 @@ class Supplier(TransactionBase):
msgprint(_("Series is mandatory"), raise_exception=1)
validate_party_accounts(self)
self.validate_internal_supplier()
def validate_internal_supplier(self):
if self.is_internal_supplier and frappe.db.get_value("Supplier", {"represents_company": self.represents_company}, "name"):
frappe.throw(_("Internal Supplier for company {0} already exists").format(
frappe.bold(self.represents_company)))
def on_trash(self):
delete_contact_and_address('Supplier', self.name)

View File

@ -107,6 +107,8 @@ class AccountsController(TransactionBase):
else:
self.validate_deferred_start_and_end_date()
self.set_inter_company_account()
validate_regional(self)
if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self)
@ -932,6 +934,38 @@ class AccountsController(TransactionBase):
else:
return frappe.db.get_single_value("Global Defaults", "disable_rounded_total")
def set_inter_company_account(self):
"""
Set intercompany account for inter warehouse transactions
This account will be used in case billing company and internal customer's
representation company is same
"""
if self.is_internal_transfer() and not self.unrealized_profit_loss_account:
unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account')
if not unrealized_profit_loss_account:
msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format(
frappe.bold(self.company))
frappe.throw(msg)
self.unrealized_profit_loss_account = unrealized_profit_loss_account
def is_internal_transfer(self):
"""
It will an internal transfer if its an internal customer and representation
company is same as billing company
"""
if self.doctype == 'Sales Invoice':
internal_party_field = 'is_internal_customer'
else:
internal_party_field = 'is_internal_supplier'
if self.get(internal_party_field) and (self.represents_company == self.company):
return True
return False
@frappe.whitelist()
def get_tax_rate(account_head):
return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)

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.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):
def __setup__(self):
@ -42,6 +44,7 @@ class BuyingController(StockController):
self.validate_items()
self.set_qty_as_per_stock_uom()
self.validate_stock_or_nonstock_items()
self.update_tax_category_for_internal_transfer()
self.validate_warehouse()
self.validate_from_warehouse()
self.set_supplier_address()
@ -62,7 +65,7 @@ class BuyingController(StockController):
self.set_landed_cost_voucher_amount()
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):
super(BuyingController, self).set_missing_values(for_validate)
@ -94,13 +97,23 @@ class BuyingController(StockController):
def validate_stock_or_nonstock_items(self):
if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items():
tax_for_valuation = [d for d in self.get("taxes")
msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items')
self.update_tax_category(msg)
def update_tax_category_for_internal_transfer(self):
if self.doctype == 'Purchase Invoice' and self.is_internal_transfer():
msg = _('Tax Category has been changed to "Total" as its an internal purchase.')
self.update_tax_category(msg)
def update_tax_category(self, msg):
tax_for_valuation = [d for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]]
if tax_for_valuation:
for d in tax_for_valuation:
d.category = 'Total'
msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items'))
if tax_for_valuation:
for d in tax_for_valuation:
d.category = 'Total'
msgprint(msg)
def validate_asset_return(self):
if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return:
@ -166,7 +179,7 @@ class BuyingController(StockController):
self.in_words = money_in_words(amount, self.currency)
# 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
stored for valuation
@ -177,7 +190,7 @@ class BuyingController(StockController):
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
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:
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
@ -187,7 +200,7 @@ class BuyingController(StockController):
if d.category in ["Valuation", "Valuation and Total"]])
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:
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
@ -205,16 +218,34 @@ class BuyingController(StockController):
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)
rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \
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)
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
+ flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
else:
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(d.consumed_qty) * flt(d.rate)
supplied_items_cost += flt(d.amount)
return supplied_items_cost
def validate_for_subcontracting(self):
if not self.is_subcontracted and self.sub_contracted_items:
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
@ -341,35 +372,17 @@ class BuyingController(StockController):
else:
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.update(raw_material_data)
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.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):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
@ -378,7 +391,7 @@ class BuyingController(StockController):
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
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)
raw_materials_cost = 0
@ -395,7 +408,7 @@ class BuyingController(StockController):
reserve_warehouse = None
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):
alternative_item_data = used_alternative_items.get(bom_item.item_code)
bom_item.item_code = alternative_item_data.item_code
@ -423,9 +436,7 @@ class BuyingController(StockController):
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
if self.doctype == "Purchase Order" and not rm.reserve_warehouse:
rm.reserve_warehouse = reserve_warehouse
rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@ -433,29 +444,8 @@ class BuyingController(StockController):
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:
rm.batch_no = item.batch_no
# get raw materials rate
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
elif not rm.reserve_warehouse:
rm.reserve_warehouse = reserve_warehouse
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"""
@ -568,7 +558,8 @@ class BuyingController(StockController):
or (cint(self.is_return) and self.docstatus==2)):
from_warehouse_sle = self.get_sl_entries(d, {
"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)
@ -578,28 +569,20 @@ class BuyingController(StockController):
"serial_no": cstr(d.serial_no).strip()
})
if self.is_return:
filters = {
"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")
outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
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:
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)
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)
@ -607,7 +590,8 @@ class BuyingController(StockController):
or (cint(self.is_return) and self.docstatus==1)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
"warehouse": d.from_warehouse
"warehouse": d.from_warehouse,
"recalculate_rate": 1
})
sl_entries.append(from_warehouse_sle)
@ -655,6 +639,7 @@ class BuyingController(StockController):
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty),
"dependant_sle_voucher_detail_no": d.reference_name
}))
def on_submit(self):
@ -846,6 +831,7 @@ class BuyingController(StockController):
else:
validate_item_type(self, "is_purchase_item", "purchase")
def get_items_from_bom(item_code, bom, exploded_item=1):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"

View File

@ -365,3 +365,45 @@ def make_return_doc(doctype, source_name, target_doc=None):
}, target_doc, set_missing_values)
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

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.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
def __setup__(self):
@ -48,6 +49,7 @@ class SellingController(StockController):
self.set_customer_address()
self.validate_for_duplicate_items()
self.validate_target_warehouse()
self.set_incoming_rate()
def set_missing_values(self, for_validate=False):
@ -230,7 +232,8 @@ class SellingController(StockController):
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'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:
il.append(frappe._dict({
@ -248,7 +251,8 @@ class SellingController(StockController):
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'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
@ -307,69 +311,89 @@ class SellingController(StockController):
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):
self.update_reserved_qty()
sl_entries = []
# Loop over items and packed items table
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 flt(d.conversion_factor)==0.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,
self.return_against, against_document_no)
# On cancellation or if return entry submission, make stock ledger entry for
# On cancellation or return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly
if d.warehouse and ((not cint(self.is_return) and self.docstatus==1)
or (cint(self.is_return) and self.docstatus==2)):
sl_entries.append(self.get_sl_entries(d, {
"actual_qty": -1*flt(d.qty),
"incoming_rate": return_rate
}))
sl_entries.append(self.get_sle_for_source_warehouse(d))
if d.target_warehouse:
target_warehouse_sle = self.get_sl_entries(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)
sl_entries.append(self.get_sle_for_target_warehouse(d))
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
or (cint(self.is_return) and self.docstatus==1)):
sl_entries.append(self.get_sl_entries(d, {
"actual_qty": -1*flt(d.qty),
"incoming_rate": return_rate
}))
sl_entries.append(self.get_sle_for_source_warehouse(d))
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):
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
if for_validate and self.po_no:
@ -463,4 +487,4 @@ def set_default_income_account_for_item(obj):
for d in obj.get("items"):
if d.item_code:
if getattr(d, "income_account", None):
set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
set_item_default(d.item_code, obj.company, 'income_account', d.income_account)

View File

@ -254,22 +254,26 @@ class StatusUpdater(Document):
if not args.get("second_source_extra_cond"):
args["second_source_extra_cond"] = ""
args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
args['second_source_condition'] = frappe.db.sql(""" select ifnull((select sum(%(second_source_field)s)
from `tab%(second_source_dt)s`
where `%(second_join_field)s`="%(detail_id)s"
and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args
and (`tab%(second_source_dt)s`.docstatus=1)
%(second_source_extra_cond)s), 0) """ % args)[0][0]
if args['detail_id']:
if not args.get("extra_cond"): args["extra_cond"] = ""
frappe.db.sql("""update `tab%(target_dt)s`
set %(target_field)s = (
args["source_dt_value"] = frappe.db.sql("""
(select ifnull(sum(%(source_field)s), 0)
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
and (docstatus=1 %(cond)s) %(extra_cond)s)
%(second_source_condition)s
)
%(update_modified)s
""" % args)[0][0] or 0.0
if args['second_source_condition']:
args["source_dt_value"] += flt(args['second_source_condition'])
frappe.db.sql("""update `tab%(target_dt)s`
set %(target_field)s = %(source_dt_value)s %(update_modified)s
where name='%(detail_id)s'""" % args)
def _update_percent_field_in_targets(self, args, update_modified=True):

View File

@ -24,7 +24,7 @@ class StockController(AccountsController):
self.validate_serialized_batch()
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:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@ -34,12 +34,12 @@ class StockController(AccountsController):
if self.docstatus==1:
if not gl_entries:
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:
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):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@ -70,14 +70,13 @@ class StockController(AccountsController):
gl_list = []
warehouse_with_no_account = []
precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
# from warehouse account/ target warehouse account
# from warehouse account
self.check_expense_account(item_row)
@ -92,9 +91,16 @@ class StockController(AccountsController):
sle = self.update_stock_ledger_entries(sle)
# expense account/ target_warehouse / source_warehouse
if item_row.get('target_warehouse'):
warehouse = item_row.get('target_warehouse')
expense_account = warehouse_account[warehouse]["account"]
else:
expense_account = item_row.expense_account
gl_list.append(self.get_gl_dict({
"account": warehouse_account[sle.warehouse]["account"],
"against": item_row.expense_account,
"against": expense_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
"remarks": self.get("remarks") or "Accounting Entry for Stock",
@ -102,9 +108,8 @@ class StockController(AccountsController):
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
}, warehouse_account[sle.warehouse]["account_currency"], item=item_row))
# expense account
gl_list.append(self.get_gl_dict({
"account": item_row.expense_account,
"account": expense_account,
"against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
@ -119,7 +124,7 @@ class StockController(AccountsController):
if warehouse_with_no_account:
for wh in warehouse_with_no_account:
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)
@ -303,23 +308,6 @@ class StockController(AccountsController):
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):
from erpnext.stock.utils import validate_warehouse_company
@ -403,19 +391,64 @@ class StockController(AccountsController):
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
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
def repost_future_sle_and_gle(self):
args = frappe._dict({
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company
})
if check_if_future_sle_exists(args):
create_repost_item_valuation_entry(args)
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
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

@ -519,6 +519,17 @@ class calculate_taxes_and_totals(object):
if self.doc.docstatus == 0:
self.calculate_outstanding_amount()
def is_internal_invoice(self):
"""
Checks if its an internal transfer invoice
and decides if to calculate any out standing amount or not
"""
if self.doc.doctype in ('Sales Invoice', 'Purchase Invoice') and self.doc.is_internal_transfer():
return True
return False
def calculate_outstanding_amount(self):
# NOTE:
# write_off_amount is only for POS Invoice
@ -526,7 +537,8 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype == "Sales Invoice":
self.calculate_paid_amount()
if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return
if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos') or \
self.is_internal_invoice(): return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
self._set_in_company_currency(self.doc, ['write_off_amount'])

View File

@ -126,7 +126,7 @@ class Appointment(Document):
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
'assign_to': existing_assignee
'assign_to': [existing_assignee]
})
return
if self._assign:
@ -139,7 +139,7 @@ class Appointment(Document):
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
'assign_to': agent
'assign_to': [agent]
})
break

View File

@ -260,6 +260,15 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings):
"""Shipping lines represents the shipping details,
each such shipping detail consists of a list of tax_lines"""
for shipping_charge in shipping_lines:
if shipping_charge.get("price"):
taxes.append({
"charge_type": _("Actual"),
"account_head": get_tax_account_head(shipping_charge),
"description": shipping_charge["title"],
"tax_amount": shipping_charge["price"],
"cost_center": shopify_settings.cost_center
})
for tax in shipping_charge.get("tax_lines"):
taxes.append({
"charge_type": _("Actual"),

View File

@ -30,6 +30,11 @@
"label": "Laboratory",
"links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test\",\n\t\t\"label\": \"Lab Test\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Sample Collection\",\n\t\t\"label\": \"Sample Collection\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Dosage Form\",\n\t\t\"label\": \"Dosage Form\"\n\t}\n]"
},
{
"hidden": 0,
"label": "Inpatient",
"links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Order\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Entry\",\n\t\t\"label\": \"Inpatient Medication Entry\"\n\t}\n]"
},
{
"hidden": 0,
"label": "Rehabilitation and Physiotherapy",
@ -38,7 +43,7 @@
{
"hidden": 0,
"label": "Records and History",
"links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]"
"links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t}\n]"
},
{
"hidden": 0,
@ -64,7 +69,7 @@
"idx": 0,
"is_standard": 1,
"label": "Healthcare",
"modified": "2020-11-23 23:00:48.764377",
"modified": "2020-11-26 22:09:09.164584",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare",

View File

@ -29,6 +29,29 @@ frappe.ui.form.on('Inpatient Medication Entry', {
}
};
});
if (frm.doc.__islocal || frm.doc.docstatus !== 0 || !frm.doc.update_stock)
return;
frm.add_custom_button(__('Make Stock Entry'), function() {
frappe.call({
method: 'erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry.make_difference_stock_entry',
args: { docname: frm.doc.name },
freeze: true,
callback: function(r) {
if (r.message) {
var doclist = frappe.model.sync(r.message);
frappe.set_route('Form', doclist[0].doctype, doclist[0].name);
} else {
frappe.msgprint({
title: __('No Drug Shortage'),
message: __('All the drugs are available with sufficient qty to process this Inpatient Medication Entry.'),
indicator: 'green'
});
}
}
});
});
},
patient: function(frm) {

View File

@ -142,25 +142,32 @@ class InpatientMedicationEntry(Document):
return orders, order_entry_map
def check_stock_qty(self):
from erpnext.stock.stock_ledger import NegativeStockError
drug_shortage = get_drug_shortage_map(self.medication_orders, self.warehouse)
drug_availability = dict()
for d in self.medication_orders:
if not drug_availability.get(d.drug_code):
drug_availability[d.drug_code] = 0
drug_availability[d.drug_code] += flt(d.dosage)
if drug_shortage:
message = _('Quantity not available for the following items in warehouse {0}. ').format(frappe.bold(self.warehouse))
message += _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.')
for drug, dosage in drug_availability.items():
available_qty = get_latest_stock_qty(drug, self.warehouse)
formatted_item_rows = ''
# validate qty
if flt(available_qty) < flt(dosage):
frappe.throw(_('Quantity not available for {0} in warehouse {1}').format(
frappe.bold(drug), frappe.bold(self.warehouse))
+ '<br><br>' + _('Available quantity is {0}, you need {1}').format(
frappe.bold(available_qty), frappe.bold(dosage))
+ '<br><br>' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'),
NegativeStockError, title=_('Insufficient Stock'))
for drug, shortage_qty in drug_shortage.items():
item_link = get_link_to_form('Item', drug)
formatted_item_rows += """
<td>{0}</td>
<td>{1}</td>
</tr>""".format(item_link, frappe.bold(shortage_qty))
message += """
<table class='table'>
<thead>
<th>{0}</th>
<th>{1}</th>
</thead>
{2}
</table>
""".format(_('Drug Code'), _('Shortage Qty'), formatted_item_rows)
frappe.throw(message, title=_('Insufficient Stock'), is_minimizable=True, wide=True)
def make_stock_entry(self):
stock_entry = frappe.new_doc('Stock Entry')
@ -223,7 +230,8 @@ def get_pending_medication_orders(entry):
for doc in data:
inpatient_record = doc.inpatient_record
doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
if inpatient_record:
doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
if entry.service_unit and doc.service_unit != entry.service_unit:
to_remove.append(doc)
@ -276,4 +284,55 @@ def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
if ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit
return
return
def get_drug_shortage_map(medication_orders, warehouse):
"""
Returns a dict like { drug_code: shortage_qty }
"""
drug_requirement = dict()
for d in medication_orders:
if not drug_requirement.get(d.drug_code):
drug_requirement[d.drug_code] = 0
drug_requirement[d.drug_code] += flt(d.dosage)
drug_shortage = dict()
for drug, required_qty in drug_requirement.items():
available_qty = get_latest_stock_qty(drug, warehouse)
if flt(required_qty) > flt(available_qty):
drug_shortage[drug] = flt(flt(required_qty) - flt(available_qty))
return drug_shortage
@frappe.whitelist()
def make_difference_stock_entry(docname):
doc = frappe.get_doc('Inpatient Medication Entry', docname)
drug_shortage = get_drug_shortage_map(doc.medication_orders, doc.warehouse)
if not drug_shortage:
return None
stock_entry = frappe.new_doc('Stock Entry')
stock_entry.purpose = 'Material Transfer'
stock_entry.set_stock_entry_type()
stock_entry.to_warehouse = doc.warehouse
stock_entry.company = doc.company
cost_center = frappe.get_cached_value('Company', doc.company, 'cost_center')
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', doc.company)
for drug, shortage_qty in drug_shortage.items():
se_child = stock_entry.append('items')
se_child.item_code = drug
se_child.item_name = frappe.db.get_value('Item', drug, 'stock_uom')
se_child.uom = frappe.db.get_value('Item', drug, 'stock_uom')
se_child.stock_uom = se_child.uom
se_child.qty = flt(shortage_qty)
se_child.t_warehouse = doc.warehouse
# in stock uom
se_child.conversion_factor = 1
se_child.cost_center = cost_center
se_child.expense_account = expense_account
return stock_entry

View File

@ -9,6 +9,7 @@ from frappe.utils import add_days, getdate, now_datetime
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_drug_shortage_map, make_difference_stock_entry
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
class TestInpatientMedicationEntry(unittest.TestCase):
@ -82,6 +83,39 @@ class TestInpatientMedicationEntry(unittest.TestCase):
self.assertEqual(stock_entry.items[0].patient, self.patient)
self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name)
def test_drug_shortage_stock_entry(self):
ipmo = create_ipmo(self.patient)
ipmo.submit()
ipmo.reload()
date = add_days(getdate(), -1)
filters = frappe._dict(
from_date=date,
to_date=date,
from_time='',
to_time='',
item_code='Dextromethorphan',
patient=self.patient
)
# check drug shortage
ipme = create_ipme(filters, update_stock=1)
ipme.warehouse = 'Finished Goods - _TC'
ipme.save()
drug_shortage = get_drug_shortage_map(ipme.medication_orders, ipme.warehouse)
self.assertEqual(drug_shortage.get('Dextromethorphan'), 3)
# check material transfer for drug shortage
make_stock_entry()
stock_entry = make_difference_stock_entry(ipme.name)
self.assertEqual(stock_entry.items[0].item_code, 'Dextromethorphan')
self.assertEqual(stock_entry.items[0].qty, 3)
stock_entry.from_warehouse = 'Stores - _TC'
stock_entry.submit()
ipme.reload()
ipme.submit()
def tearDown(self):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
@ -94,15 +128,12 @@ class TestInpatientMedicationEntry(unittest.TestCase):
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name})
doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
doc.delete()
def make_stock_entry():
def make_stock_entry(warehouse=None):
frappe.db.set_value('Company', '_Test Company', {
'stock_adjustment_account': 'Stock Adjustment - _TC',
'default_inventory_account': 'Stock In Hand - _TC'
@ -110,7 +141,7 @@ def make_stock_entry():
stock_entry = frappe.new_doc('Stock Entry')
stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.company = '_Test Company'
stock_entry.to_warehouse = 'Stores - _TC'
stock_entry.to_warehouse = warehouse or 'Stores - _TC'
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company')
se_child = stock_entry.append('items')
se_child.item_code = 'Dextromethorphan'

View File

@ -18,6 +18,10 @@ def get_data():
{
'label': _('Billing'),
'items': ['Sales Invoice']
},
{
'label': _('Orders'),
'items': ['Inpatient Medication Order']
}
]
}

View File

@ -18,13 +18,18 @@ frappe.ui.form.on('Employee Advance', {
if (!frm.doc.employee) {
frappe.msgprint(__("Please select employee first"));
}
var company_currency = erpnext.get_currency(frm.doc.company);
let company_currency = erpnext.get_currency(frm.doc.company);
let currencies = [company_currency];
if (frm.doc.currency && (frm.doc.currency != company_currency)) {
currencies.push(frm.doc.currency);
}
return {
filters: {
"root_type": "Asset",
"is_group": 0,
"company": frm.doc.company,
"account_currency": ["in", [frm.doc.currency, company_currency]],
"account_currency": ["in", currencies],
}
};
});
@ -181,21 +186,23 @@ frappe.ui.form.on('Employee Advance', {
},
currency: function(frm) {
var from_currency = frm.doc.currency;
var company_currency;
if (!frm.doc.company) {
company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
} else {
company_currency = erpnext.get_currency(frm.doc.company);
if (frm.doc.currency) {
var from_currency = frm.doc.currency;
var company_currency;
if (!frm.doc.company) {
company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
} else {
company_currency = erpnext.get_currency(frm.doc.company);
}
if (from_currency != company_currency) {
frm.events.set_exchange_rate(frm, from_currency, company_currency);
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" );
}
frm.refresh_fields();
}
if (from_currency != company_currency) {
frm.events.set_exchange_rate(frm, from_currency, company_currency);
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" );
}
frm.refresh_fields();
},
set_exchange_rate: function(frm, from_currency, company_currency) {

View File

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

View File

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

View File

@ -8,7 +8,17 @@ frappe.views.calendar["Job Card"] = {
"allDay": "allDay",
"progress": "progress"
},
gantt: true,
gantt: {
field_map: {
"start": "started_time",
"end": "started_time",
"id": "name",
"title": "subject",
"color": "color",
"allDay": "allDay",
"progress": "progress"
}
},
filters: [
{
"fieldtype": "Link",

View File

@ -6,7 +6,6 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
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,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry
@ -18,7 +17,6 @@ from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestWorkOrder(unittest.TestCase):
def setUp(self):
set_perpetual_inventory(0)
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
@ -491,6 +489,39 @@ class TestWorkOrder(unittest.TestCase):
work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
def test_partial_material_consumption(self):
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
ste_cancel_list = []
ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0)
ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0)
ste_cancel_list.extend([ste1, ste2])
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4))
s.submit()
ste_cancel_list.append(s)
ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
ste1.submit()
ste_cancel_list.append(ste1)
print(wo_order.name)
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
self.assertEquals(ste3.fg_completed_qty, 2)
expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4}
for row in ste3.items:
self.assertEquals(row.qty, expected_qty.get(row.item_code))
for ste_doc in ste_cancel_list:
ste_doc.cancel()
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0)
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`

View File

@ -545,7 +545,8 @@ erpnext.work_order = {
var tbl = frm.doc.required_items || [];
var tbl_lenght = tbl.length;
for (var i = 0, len = tbl_lenght; i < len; i++) {
if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1;
}
}

View File

@ -59,7 +59,7 @@ class Member(Document):
frappe.msgprint(_("A customer is already linked to this Member"))
cust = create_customer(frappe._dict({
'fullname': self.member_name,
'email': self.email_id or self.user,
'email': self.email_id or self.email,
'phone': None
}))
@ -177,4 +177,4 @@ def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, m
mobile=mobile
))
return member.name
return member.name

View File

@ -450,7 +450,6 @@ erpnext.patches.v8_9.set_member_party_type
erpnext.patches.v9_0.add_user_to_child_table_in_pos_profile
erpnext.patches.v9_0.set_schedule_date_for_material_request_and_purchase_order
erpnext.patches.v9_0.student_admission_childtable_migrate
erpnext.patches.v9_0.fix_subscription_next_date #2017-10-23
erpnext.patches.v9_0.add_healthcare_domain
erpnext.patches.v9_0.set_variant_item_description
erpnext.patches.v9_0.set_uoms_in_variant_field

View File

@ -5,6 +5,8 @@ from frappe.utils import nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
from erpnext.loan_management.doctype.loan.loan import make_repayment_entry
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import get_accrued_interest_entries
from frappe.model.naming import make_autoname
def execute():
@ -18,15 +20,29 @@ def execute():
frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail')
frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual')
frappe.reload_doc('accounts', 'doctype', 'gl_entry')
frappe.reload_doc('accounts', 'doctype', 'journal_entry_account')
updated_loan_types = []
loans_to_close = []
# Update old loan status as closed
if frappe.db.has_column('Repayment Schedule', 'paid'):
loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule`
where paid = 0 and docstatus = 1""", as_dict=1)
loans_to_close = [d.parent for d in loans_list]
if loans_to_close:
frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close))
loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment',
'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'])
'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'],
filters={'docstatus': 1, 'status': ('!=', 'Closed')})
for loan in loans:
# Update details in Loan Types and Loan
loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company')
loan_type = loan.loan_type
group_income_account = frappe.get_value('Account', {'company': loan.company,
'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')})
@ -38,7 +54,26 @@ def execute():
penalty_account = create_account(company=loan.company, account_type='Income Account',
account_name='Penalty Account', parent_account=group_income_account)
if not loan_type_company:
# Same loan type used for multiple companies
if loan_type_company and loan_type_company != loan.company:
# get loan type for appropriate company
loan_type_name = frappe.get_value('Loan Type', {'company': loan.company,
'mode_of_payment': loan.mode_of_payment, 'loan_account': loan.loan_account,
'payment_account': loan.payment_account, 'interest_income_account': loan.interest_income_account,
'penalty_income_account': loan.penalty_income_account}, 'name')
if not loan_type_name:
loan_type_name = create_loan_type(loan, loan_type_name, penalty_account)
# update loan type in loan
frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name,
loan.name))
loan_type = loan_type_name
if loan_type_name not in updated_loan_types:
updated_loan_types.append(loan_type_name)
elif not loan_type_company:
loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type)
loan_type_doc.is_term_loan = 1
loan_type_doc.company = loan.company
@ -49,8 +84,9 @@ def execute():
loan_type_doc.penalty_income_account = penalty_account
loan_type_doc.submit()
updated_loan_types.append(loan.loan_type)
loan_type = loan.loan_type
if loan.loan_type in updated_loan_types:
if loan_type in updated_loan_types:
if loan.status == 'Fully Disbursed':
status = 'Disbursed'
elif loan.status == 'Repaid/Closed':
@ -64,25 +100,48 @@ def execute():
'status': status
})
process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan.loan_type,
process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type,
loan=loan.name)
payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date
FROM `tabJournal Entry` j, `tabJournal Entry Account` a
WHERE a.parent = j.name and a.reference_type='Loan' and a.reference_name = %s
and account = %s
''', (loan.name, loan.loan_account), as_dict=1)
for payment in payments:
repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant,
loan.loan_type, loan.company)
if frappe.db.has_column('Repayment Schedule', 'paid'):
total_principal, total_interest = frappe.db.get_value('Repayment Schedule', {'paid': 1, 'parent': loan.name},
['sum(principal_amount) as total_principal', 'sum(interest_amount) as total_interest'])
repayment_entry.amount_paid = payment.debit_in_account_currency
repayment_entry.posting_date = payment.posting_date
repayment_entry.save()
repayment_entry.submit()
accrued_entries = get_accrued_interest_entries(loan.name)
for entry in accrued_entries:
interest_paid = 0
principal_paid = 0
jv = frappe.get_doc('Journal Entry', payment.name)
jv.flags.ignore_links = True
jv.cancel()
if total_interest > entry.interest_amount:
interest_paid = entry.interest_amount
else:
interest_paid = total_interest
if total_principal > entry.payable_principal_amount:
principal_paid = entry.payable_principal_amount
else:
principal_paid = total_principal
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
(principal_paid, interest_paid, entry.name))
total_principal -= principal_paid
total_interest -= interest_paid
def create_loan_type(loan, loan_type_name, penalty_account):
loan_type_doc = frappe.new_doc('Loan Type')
loan_type_doc.loan_name = make_autoname("Loan Type-.####")
loan_type_doc.is_term_loan = 1
loan_type_doc.company = loan.company
loan_type_doc.mode_of_payment = loan.mode_of_payment
loan_type_doc.payment_account = loan.payment_account
loan_type_doc.loan_account = loan.loan_account
loan_type_doc.interest_income_account = loan.interest_income_account
loan_type_doc.penalty_income_account = penalty_account
loan_type_doc.submit()
return loan_type_doc.name

View File

@ -1,48 +0,0 @@
# Copyright (c) 2017, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import getdate
from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
def execute():
frappe.reload_doc('accounts', 'doctype', 'subscription')
fields = ["name", "reference_doctype", "reference_document",
"start_date", "frequency", "repeat_on_day"]
for d in fields:
if not frappe.db.has_column('Subscription', d):
return
doctypes = ('Purchase Order', 'Sales Order', 'Purchase Invoice', 'Sales Invoice')
for data in frappe.get_all('Subscription',
fields = fields,
filters = {'reference_doctype': ('in', doctypes), 'docstatus': 1}):
recurring_id = frappe.db.get_value(data.reference_doctype, data.reference_document, "recurring_id")
if recurring_id:
frappe.db.sql("update `tab{0}` set subscription=%s where recurring_id=%s"
.format(data.reference_doctype), (data.name, recurring_id))
date_field = 'transaction_date'
if data.reference_doctype in ['Sales Invoice', 'Purchase Invoice']:
date_field = 'posting_date'
start_date = frappe.db.get_value(data.reference_doctype, data.reference_document, date_field)
if start_date and getdate(start_date) != getdate(data.start_date):
last_ref_date = frappe.db.sql("""
select {0}
from `tab{1}`
where subscription=%s and docstatus < 2
order by creation desc
limit 1
""".format(date_field, data.reference_doctype), data.name)[0][0]
next_schedule_date = get_next_schedule_date(last_ref_date, data.frequency, data.repeat_on_day)
frappe.db.set_value("Subscription", data.name, {
"start_date": start_date,
"next_schedule_date": next_schedule_date
}, None)

View File

@ -12,14 +12,6 @@ frappe.ui.form.on('Additional Salary', {
}
};
});
if (!frm.doc.currency) return;
frm.set_query("salary_component", function() {
return {
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {currency: frm.doc.currency, company: frm.doc.company}
};
});
},
employee: function(frm) {

View File

@ -23,6 +23,7 @@
"employee_benefits",
"totals",
"total_amount",
"column_break",
"pro_rata_dispensed_amount"
],
"fields": [
@ -139,11 +140,15 @@
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break",
"fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-11-25 11:49:05.095101",
"modified": "2020-12-14 15:52:08.566418",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Application",

View File

@ -11,11 +11,11 @@ frappe.ui.form.on('Employee Incentive', {
};
});
if (!frm.doc.currency) return;
if (!frm.doc.company) return;
frm.set_query("salary_component", function() {
return {
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
filters: {type: "earning", company: frm.doc.company}
};
});

View File

@ -4,9 +4,13 @@
frappe.ui.form.on('Retention Bonus', {
setup: function(frm) {
frm.set_query("employee", function() {
if (!frm.doc.company) {
frappe.msgprint(__("Please Select Company First"));
}
return {
filters: {
"status": "Active"
"status": "Active",
"company": frm.doc.company
}
};
});

View File

@ -55,17 +55,17 @@ frappe.ui.form.on('Salary Structure', {
},
set_earning_deduction_component: function(frm) {
if(!frm.doc.currency && !frm.doc.company) return;
if(!frm.doc.company) return;
frm.set_query("salary_component", "earnings", function() {
return {
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
filters: {type: "earning", company: frm.doc.company}
};
});
frm.set_query("salary_component", "deductions", function() {
return {
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
filters: {type: "deduction", currency: frm.doc.currency, company: frm.doc.company}
filters: {type: "deduction", company: frm.doc.company}
};
});
},
@ -74,7 +74,6 @@ frappe.ui.form.on('Salary Structure', {
currency: function(frm) {
calculate_totals(frm.doc);
frm.trigger("set_dynamic_labels")
frm.trigger('set_earning_deduction_component');
frm.refresh()
},

View File

@ -210,7 +210,7 @@ def get_employees(salary_structure):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters):
if len(filters) < 3:
if len(filters) < 2:
return {}
return frappe.db.sql("""

View File

@ -609,6 +609,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.calculate_outstanding_amount(update_paid_amount);
},
is_internal_invoice: function() {
if (['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
if (this.frm.doc.company === this.frm.doc.represents_company) {
return true;
}
}
return false;
},
calculate_outstanding_amount: function(update_paid_amount) {
// NOTE:
// paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice
@ -617,7 +626,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.calculate_paid_amount();
}
if(this.frm.doc.is_return || this.frm.doc.docstatus > 0) return;
if (this.frm.doc.is_return || (this.frm.doc.docstatus > 0) || this.is_internal_invoice()) return;
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);

View File

@ -20,4 +20,4 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( {
});
}
}
});
});

View File

@ -12,6 +12,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
tax_category: function(frm) {
frm.trigger('get_tax_template');
},
customer_address: function(frm) {
frm.trigger('get_tax_template');
},
get_tax_template: function(frm) {
if (!frm.doc.company) return;

View File

@ -53,7 +53,7 @@ def validate_gstin_for_india(doc, method):
.format(doc.gst_state_number))
def validate_tax_category(doc, method):
if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
if doc.is_inter_state:
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
else:

View File

@ -151,6 +151,7 @@ class Gstr1Report(object):
{select_columns}
from `tab{doctype}`
where docstatus = 1 {where_conditions}
and is_opening = 'No'
order by posting_date desc
""".format(select_columns=self.select_columns, doctype=self.doctype,
where_conditions=conditions), self.filters, as_dict=1)

View File

@ -58,6 +58,7 @@ class Customer(TransactionBase):
self.set_loyalty_program()
self.check_customer_group_change()
self.validate_default_bank_account()
self.validate_internal_customer()
# set loyalty program tier
if frappe.db.exists('Customer', self.name):
@ -82,6 +83,11 @@ class Customer(TransactionBase):
if not is_company_account:
frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)))
def validate_internal_customer(self):
if self.is_internal_customer and frappe.db.get_value('Customer', {"represents_company": self.represents_company}, "name"):
frappe.throw(_("Internal Customer for company {0} already exists").format(
frappe.bold(self.represents_company)))
def on_update(self):
self.validate_name_with_customer_group()
self.create_primary_contact()
@ -398,7 +404,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
# form a list of emails and names to show to the user
credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users]
if not credit_controller_users_formatted:
frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer)))
frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer))
message = """Please contact any of the following users to extend the credit limits for {0}:
<br><br><ul><li>{1}</li></ul>""".format(customer, '<li>'.join(credit_controller_users_formatted))

View File

@ -14,7 +14,6 @@ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty
from frappe.desk.notifications import clear_doctype_notifications
from frappe.contacts.doctype.address.address import get_company_address
from erpnext.controllers.selling_controller import SellingController
from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@ -418,8 +417,7 @@ class SalesOrder(SellingController):
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
delivery_date = get_next_schedule_date(ref_doc_delivery_date,
auto_repeat_doc.frequency, auto_repeat_doc.start_date, cint(auto_repeat_doc.repeat_on_day))
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
if delivery_date <= transaction_date:
delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date)

View File

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

View File

@ -274,7 +274,8 @@ erpnext.company.setup_queries = function(frm) {
["default_employee_advance_account", {"root_type": "Asset"}],
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}]
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
["unrealized_profit_loss_account", {"root_type": "Liability"}]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});

View File

@ -46,10 +46,9 @@
"round_off_account",
"round_off_cost_center",
"write_off_account",
"discount_allowed_account",
"discount_received_account",
"exchange_gain_loss_account",
"unrealized_exchange_gain_loss_account",
"unrealized_profit_loss_account",
"column_break0",
"allow_account_creation_against_child_company",
"default_payable_account",
@ -261,14 +260,14 @@
{
"fieldname": "create_chart_of_accounts_based_on",
"fieldtype": "Select",
"label": "Create Chart of Accounts Based on",
"label": "Create Chart Of Accounts Based On",
"options": "\nStandard Template\nExisting Company"
},
{
"depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"",
"fieldname": "chart_of_accounts",
"fieldtype": "Select",
"label": "Chart of Accounts Template",
"label": "Chart Of Accounts Template",
"no_copy": 1
},
{
@ -345,18 +344,6 @@
"label": "Write Off Account",
"options": "Account"
},
{
"fieldname": "discount_allowed_account",
"fieldtype": "Link",
"label": "Discount Allowed Account",
"options": "Account"
},
{
"fieldname": "discount_received_account",
"fieldtype": "Link",
"label": "Discount Received Account",
"options": "Account"
},
{
"fieldname": "exchange_gain_loss_account",
"fieldtype": "Link",
@ -740,6 +727,12 @@
"fieldtype": "Link",
"label": "Default In Transit Warehouse",
"options": "Warehouse"
},
{
"fieldname": "unrealized_profit_loss_account",
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
}
],
"icon": "fa fa-building",
@ -747,7 +740,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
"modified": "2020-08-06 00:38:08.311216",
"modified": "2020-12-03 12:27:27.085094",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@ -808,4 +801,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
}
}

View File

@ -7,7 +7,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List"
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"abbr": "_TC1",
@ -17,7 +18,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List"
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"abbr": "_TC2",
@ -27,7 +29,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List"
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"abbr": "_TC3",
@ -38,7 +41,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List"
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"abbr": "_TC4",
@ -50,7 +54,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List"
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"abbr": "_TC5",
@ -61,7 +66,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
"default_holiday_list": "_Test Holiday List"
"default_holiday_list": "_Test Holiday List",
"enable_perpetual_inventory": 0
},
{
"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 frappe.utils import cint, flt
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestBatch(unittest.TestCase):
def setUp(self):
set_perpetual_inventory(0)
def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({
"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):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after
from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
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({
"item_code": self.item_code,
"warehouse": self.warehouse,
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"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)
# Update qty_after_transaction in future SLEs of this item and warehouse
update_qty_in_future_sle(args)
def update_qty(self, args):
# update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation":

View File

@ -217,6 +217,7 @@ class DeliveryNote(SellingController):
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
def on_cancel(self):
super(DeliveryNote, self).on_cancel()
@ -234,7 +235,8 @@ class DeliveryNote(SellingController):
self.cancel_packing_slips()
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):
from erpnext.selling.doctype.customer.customer import check_credit_limit

View File

@ -10,8 +10,7 @@ import frappe.defaults
from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
import get_gl_entries, set_perpetual_inventory
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice, make_delivery_trip
from erpnext.stock.doctype.stock_entry.test_stock_entry \
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
class TestDeliveryNote(unittest.TestCase):
def setUp(self):
set_perpetual_inventory(0)
def test_over_billing_against_dn(self):
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):
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)
stock_queue = json.loads(get_previous_sle({

View File

@ -56,6 +56,7 @@
"base_net_rate",
"base_net_amount",
"billed_amt",
"incoming_rate",
"item_weight_details",
"weight_per_unit",
"total_weight",
@ -732,16 +733,22 @@
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"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,
"print_hide": 1,
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-07-31 20:12:43.054342",
"modified": "2020-12-07 19:59:27.119856",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",

View File

@ -458,5 +458,15 @@
"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.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry
import unittest
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestItemAlternative(unittest.TestCase):
def setUp(self):
set_perpetual_inventory(0)
make_items()
def test_alternative_item_for_subcontract_rm(self):

View File

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

View File

@ -77,9 +77,9 @@ class LandedCostVoucher(Document):
company_currency = erpnext.get_company_currency(self.company)
for account in self.taxes:
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.
Please select expense account with account currency as {1}""")
.format(account.idx, frappe.bold(company_currency)), title=_("Invalid Account 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 {}.").format(frappe.bold(company_currency)),
title=_("Invalid Account Currency"))
def set_total_taxes_and_charges(self):
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()
# 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
for item in doc.get("items"):
@ -143,6 +143,7 @@ class LandedCostVoucher(Document):
doc.docstatus = 1
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
doc.make_gl_entries()
doc.repost_future_sle_and_gle()
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
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,
'item_code': item.item_code }, fields=['name', 'docstatus'])
if not docs or len(docs) != item.qty:
frappe.throw(_('There are not enough asset created or linked to {0}. \
Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty))
frappe.throw(_('There are not enough asset created or linked to {0}.').format(item.receipt_document)
+ _('Please create or link {0} Assets with respective document.').format(item.qty))
if docs:
for d in docs:
if d.docstatus == 1:
frappe.throw(_('{2} <b>{0}</b> has submitted Assets.\
Remove Item <b>{1}</b> from table to continue.').format(
item.receipt_document, item.item_code, item.receipt_document_type))
frappe.throw(_('{0} {1} has submitted Assets. Remove Item {2} from table to continue.')
.format(item.receipt_document_type, frappe.bold(item.receipt_document), frappe.bold(item.item_code)))
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
for item in receipt_document.get("items"):

View File

@ -7,7 +7,7 @@ import unittest
import frappe
from frappe.utils import flt
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.account.test_account import get_inventory_account
@ -27,7 +27,7 @@ class TestLandedCostVoucher(unittest.TestCase):
},
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")
self.assertEqual(pr_lc_value, 25.0)
@ -89,7 +89,7 @@ class TestLandedCostVoucher(unittest.TestCase):
},
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},
"landed_cost_voucher_amount")
@ -137,7 +137,7 @@ class TestLandedCostVoucher(unittest.TestCase):
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",
["warehouse", "purchase_rate"], as_dict=1)
@ -160,7 +160,7 @@ class TestLandedCostVoucher(unittest.TestCase):
})
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[2].applicable_charges, 41.08)
@ -236,7 +236,7 @@ def make_landed_cost_voucher(** args):
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)
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
class TestMaterialRequest(unittest.TestCase):
def setUp(self):
erpnext.set_perpetual_inventory(0)
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()

View File

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

View File

@ -181,6 +181,7 @@ class PurchaseReceipt(BuyingController):
update_serial_nos_after_submit(self, "items")
self.make_gl_entries()
self.repost_future_sle_and_gle()
def check_next_docstatus(self):
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
self.update_stock_ledger()
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()
def get_current_stock(self):

View File

@ -9,14 +9,15 @@ import frappe.defaults
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.item.test_item import create_item
from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import make_item
from six import iteritems
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestPurchaseReceipt(unittest.TestCase):
def setUp(self):
set_perpetual_inventory(0)
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_reverse_purchase_receipt_sle(self):
@ -112,6 +113,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertFalse(get_gl_entries("Purchase Receipt", pr.name))
pr.cancel()
def test_batched_serial_no_purchase(self):
item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'})
if not item:
@ -183,22 +186,30 @@ class TestPurchaseReceipt(unittest.TestCase):
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))
pr.cancel()
def test_subcontracting_gle_fg_item_rate_zero(self):
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")
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")
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",
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)
self.assertFalse(gl_entries)
set_perpetual_inventory(0)
pr.cancel()
se1.cancel()
se2.cancel()
def test_subcontracting_over_receipt(self):
"""
@ -216,13 +227,13 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = "_Test Subcontracted FG Item 1"
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")
#stock raw materials in a warehouse before transfer
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=1, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
se1 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=10, basic_rate=100)
se2 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test FG Item", qty=1, basic_rate=100)
rm_items = [
{
@ -254,6 +265,13 @@ class TestPurchaseReceipt(unittest.TestCase):
pr1.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):
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"),
@ -284,6 +302,8 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_warehouse)
pr.cancel()
def test_purchase_return_partial(self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
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.status, 'Return Issued')
return_pr.cancel()
pr.cancel()
def test_purchase_return_for_rejected_qty(self):
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
@ -388,6 +411,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(actual_qty, -2)
return_pr.cancel()
pr.cancel()
def test_purchase_return_for_serialized_items(self):
def _check_serial_no_values(serial_no, field_values):
@ -415,6 +441,10 @@ class TestPurchaseReceipt(unittest.TestCase):
"delivery_document_no": return_pr.name
})
return_pr.cancel()
pr.reload()
pr.cancel()
def test_purchase_return_for_multi_uom(self):
item_code = "_Test Purchase Return For Multi-UOM"
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)
return_pr.cancel()
pr.cancel()
def test_closed_purchase_receipt(self):
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")
self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
pr.reload()
pr.cancel()
def test_pr_billing_status(self):
# PO -> PR1 -> PI and PO -> PI and PO -> PR2
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.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):
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",
{"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):
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
@ -519,16 +567,19 @@ class TestPurchaseReceipt(unittest.TestCase):
item_code = item.name
serial_no = random_string(5)
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)
pr1 = make_purchase_receipt(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)
self.assertRaises(SerialNoDuplicateError, pr.submit)
pr2 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
self.assertRaises(SerialNoDuplicateError, pr2.submit)
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)
self.assertRaises(SerialNoDuplicateError, se.submit)
dn.cancel()
pr1.cancel()
def test_auto_asset_creation(self):
asset_item = "Test Asset Item"
@ -549,7 +600,7 @@ class TestPurchaseReceipt(unittest.TestCase):
'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC',
'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
'depreciation_expense_account': '_Test Depreciation - _TC'
'depreciation_expense_account': '_Test Depreciations - _TC'
}]
}).insert()
@ -568,6 +619,8 @@ class TestPurchaseReceipt(unittest.TestCase):
location = frappe.db.get_value('Asset', assets[0].name, 'location')
self.assertEquals(location, "Test Location")
pr.cancel()
def test_purchase_return_with_submitted_asset(self):
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.cancel()
pr.cancel()
def test_purchase_receipt_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - TCP1"
@ -605,7 +661,8 @@ class TestPurchaseReceipt(unittest.TestCase):
'location_name': 'Test Location'
}).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)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@ -623,6 +680,8 @@ class TestPurchaseReceipt(unittest.TestCase):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
pr.cancel()
def test_purchase_receipt_cost_center_with_balance_sheet_account(self):
if not frappe.db.exists('Location', 'Test Location'):
frappe.get_doc({
@ -648,6 +707,8 @@ class TestPurchaseReceipt(unittest.TestCase):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
pr.cancel()
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
@ -663,6 +724,12 @@ class TestPurchaseReceipt(unittest.TestCase):
pi = make_purchase_invoice(pr.name)
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):
pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
pr1.append("items", {
@ -689,8 +756,14 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEquals(pi2.items[0].qty, 2)
self.assertEquals(pi2.items[1].qty, 1)
pr2.cancel()
pi1.cancel()
pr1.reload()
pr1.cancel()
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",
warehouse = "Stores - TCP1", do_not_save=1)
@ -713,18 +786,20 @@ class TestPurchaseReceipt(unittest.TestCase):
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
warehouse = frappe.get_doc('Warehouse', 'Work In Progress - TCP1')
warehouse.account = '_Test Account Stock In Hand - TCP1'
warehouse.save()
pr.cancel()
pr1.cancel()
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")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
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 = ''
@ -749,7 +824,7 @@ class TestPurchaseReceipt(unittest.TestCase):
]
expected_sle = {
'Work In Progress - TCP1': -5,
'_Test Warehouse for Valuation - TCP1': -5,
'Stores - TCP1': 5
}
@ -761,60 +836,9 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEqual(gle.debit, expected_gle[i][1])
self.assertEqual(gle.credit, expected_gle[i][2])
warehouse.account = ''
warehouse.save()
pr.cancel()
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):
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")
pr.delete()
se.cancel()
ste2.cancel()
ste1.cancel()
po.cancel()
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
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()
if 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.supplier = args.supplier or "_Test Supplier"
pr.is_subcontracted = args.is_subcontracted or "No"

View File

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

View File

@ -4,6 +4,11 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
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) {
if (frm.doc.item_code) {
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,89 @@
# -*- 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
from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.accounts.utils import update_gl_entries_after
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)
doc.set_status('Completed')
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(traceback)
frappe.db.set_value(doc.doctype, doc.name, 'error_log', traceback)
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)

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

@ -134,17 +134,13 @@ class SerialNo(StockController):
sle_dict = self.get_stock_ledger_entries(serial_no)
if sle_dict:
if sle_dict.get("incoming", []):
sle_list = [sle for sle in sle_dict["incoming"] if sle.is_cancelled == 0]
if sle_list:
entries["purchase_sle"] = sle_list[0]
entries["purchase_sle"] = sle_dict["incoming"][0]
if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
entries["last_sle"] = sle_dict["incoming"][0]
else:
entries["last_sle"] = sle_dict["outgoing"][0]
sle_list = [sle for sle in sle_dict["outgoing"] if sle.is_cancelled == 0]
if sle_list:
entries["delivery_sle"] = sle_list[0]
entries["delivery_sle"] = sle_dict["outgoing"][0]
return entries
@ -155,11 +151,12 @@ class SerialNo(StockController):
for sle in frappe.db.sql("""
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
`tabStock Ledger Entry`
WHERE
item_code=%s AND company = %s
AND is_cancelled = 0
AND (serial_no = %s
OR serial_no like %s
OR serial_no like %s
@ -179,7 +176,7 @@ class SerialNo(StockController):
def on_trash(self):
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)
# Find the exact match
@ -229,7 +226,7 @@ def validate_serial_no(sle, item_det):
if serial_nos:
frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
SerialNoNotRequiredError)
else:
elif not sle.is_cancelled:
if serial_nos:
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))
@ -247,10 +244,6 @@ def validate_serial_no(sle, item_det):
"delivery_document_no", "delivery_document_type", "warehouse",
"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 not allow_serial_nos_with_different_item(serial_no, sle):
frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
@ -277,7 +270,7 @@ def validate_serial_no(sle, item_det):
frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no,
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")
.format(serial_no), SerialNoWarehouseError)
@ -327,6 +320,12 @@ def validate_serial_no(sle, item_det):
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),
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):
sle_doc.update({
@ -334,7 +333,7 @@ def validate_material_transfer_entry(sle_doc):
"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"):
if sle_doc.actual_qty < 0:
sle_doc.skip_update_serial_no = True
@ -379,7 +378,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle):
stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no)
if stock_entry.purpose in ("Repack", "Manufacture"):
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)
if sle_serial_no in serial_nos:
allow_serial_nos = True
@ -388,7 +387,7 @@ def allow_serial_nos_with_different_item(sle_serial_no, sle):
def update_serial_nos(sle, item_det):
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:
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
frappe.db.set(sle, "serial_no", serial_nos)

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.serial_no.serial_no import get_serial_nos
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_records = frappe.get_test_records('Serial No')
@ -38,8 +37,6 @@ class TestSerialNo(unittest.TestCase):
self.assertTrue(SerialNoCannotCannotChangeError, sr.save)
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")
serial_nos = get_serial_nos(se.get("items")[0].serial_no)

View File

@ -510,22 +510,31 @@ frappe.ui.form.on('Stock Entry', {
calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm);
const total_basic_amount = frappe.utils.sum(
(frm.doc.items || []).map(function(i) { return i.t_warehouse ? flt(i.basic_amount) : 0; })
);
let total_basic_amount = 0;
if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
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) {
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;
} else {
item.additional_cost = 0;
}
item.amount = flt(item.basic_amount + flt(item.additional_cost),
precision("amount", item));
item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
if (flt(item.transfer_qty)) {
item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
@ -841,6 +850,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
}
},
fg_completed_qty: function() {
this.get_items();
},
get_items: function() {
var me = this;
if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no)
@ -850,6 +863,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
// if work order / bom is mentioned, get items
return this.frm.call({
doc: me.frm.doc,
freeze: true,
method: "get_items",
callback: function(r) {
if(!r.exc) refresh_field("items");

View File

@ -644,9 +644,10 @@
],
"icon": "fa fa-file-text",
"idx": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-08-11 19:10:07.954981",
"modified": "2020-09-09 12:59:02.508943",
"modified_by": "Administrator",
"module": "Stock",
"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 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.accounts.general_ledger import process_gl_map
import json
from six import string_types, itervalues, iteritems
@ -58,6 +58,7 @@ class StockEntry(StockController):
self.validate_warehouse()
self.validate_work_order()
self.validate_bom()
self.mark_finished_and_scrap_items()
self.validate_finished_goods()
self.validate_with_material_request()
self.validate_batch()
@ -75,13 +76,11 @@ class StockEntry(StockController):
else:
set_batch_nos(self, 's_warehouse')
self.set_incoming_rate()
self.validate_serialized_batch()
self.set_actual_qty()
self.calculate_rate_and_amount(update_finished_item_rate=False)
self.calculate_rate_and_amount()
def on_submit(self):
self.update_stock_ledger()
update_serial_nos_after_submit(self, "items")
@ -89,11 +88,15 @@ class StockEntry(StockController):
self.validate_purchase_order()
if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items()
self.make_gl_entries()
self.repost_future_sle_and_gle()
self.update_cost_in_project()
self.validate_reserved_serial_no_consumption()
self.update_transferred_qty()
self.update_quality_inspection()
if self.work_order and self.purpose == "Manufacture":
self.update_so_in_serial_number()
@ -113,13 +116,15 @@ class StockEntry(StockController):
self.update_work_order()
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.repost_future_sle_and_gle()
self.update_cost_in_project()
self.update_transferred_qty()
self.update_quality_inspection()
self.delete_auto_created_batches()
self.delete_linked_stock_entry()
if self.purpose == 'Material Transfer' and self.add_to_transit:
self.set_material_request_transfer_status('Not Started')
@ -152,6 +157,12 @@ class StockEntry(StockController):
frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
.format(self.job_card))
def delete_linked_stock_entry(self):
if self.purpose == "Send to Warehouse":
for d in frappe.get_all("Stock Entry", filters={"docstatus": 0,
"outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}):
frappe.delete_doc("Stock Entry", d.name)
def set_transfer_qty(self):
for item in self.get("items"):
if not flt(item.qty):
@ -249,11 +260,10 @@ class StockEntry(StockController):
def validate_fg_completed_qty(self):
if self.purpose == "Manufacture" and self.work_order:
production_item = frappe.get_value('Work Order', self.work_order, 'production_item')
for item in self.items:
if item.item_code == production_item and item.t_warehouse and item.qty != self.fg_completed_qty:
for d in self.items:
if d.is_finished_item and d.qty != self.fg_completed_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))
.format(d.qty, self.fg_completed_qty))
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@ -375,21 +385,6 @@ class StockEntry(StockController):
frappe.throw(_("Stock Entries already created for Work Order ")
+ 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):
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
@ -425,57 +420,64 @@ class StockEntry(StockController):
d.serial_no = transferred_serial_no
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_transfer_qty()
self.set_actual_qty()
self.calculate_rate_and_amount()
def calculate_rate_and_amount(self, force=False,
update_finished_item_rate=True, raise_error_if_no_rate=True):
self.set_basic_rate(force, update_finished_item_rate, raise_error_if_no_rate)
def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate)
self.distribute_additional_costs()
self.update_valuation_rate()
self.set_total_incoming_outgoing_value()
self.set_total_amount()
def set_basic_rate(self, force=False, update_finished_item_rate=True, raise_error_if_no_rate=True):
"""get stock and incoming rate on posting date"""
raw_material_cost = 0.0
scrap_material_cost = 0.0
fg_basic_rate = 0.0
def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
"""
Set rate for outgoing, scrapped and finished items
"""
# Set rate for outgoing items
outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate)
# Set basic rate for incoming items
for d in self.get('items'):
if d.t_warehouse: fg_basic_rate = flt(d.basic_rate)
args = self.get_args_for_incoming_rate(d)
if d.s_warehouse or d.set_basic_rate_manually: continue
# get basic rate
if not d.bom_no:
if (not flt(d.basic_rate) and not d.allow_zero_valuation_rate) or d.s_warehouse or force:
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
if d.allow_zero_valuation_rate:
d.basic_rate = 0.0
elif d.is_finished_item:
if self.purpose == "Manufacture":
d.basic_rate = self.get_basic_rate_for_manufactured_item(d.transfer_qty, outgoing_items_cost)
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"))
if not d.t_warehouse:
raw_material_cost += flt(d.basic_amount)
# 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)
outgoing_items_cost += flt(d.basic_amount)
return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
return frappe._dict({
@ -491,44 +493,44 @@ class StockEntry(StockController):
"allow_zero_valuation": item.allow_zero_valuation_rate,
})
def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0):
total_fg_qty = 0
if not raw_material_cost and self.get("items"):
raw_material_cost = sum([flt(row.basic_amount) for row in self.items
if row.s_warehouse and not row.t_warehouse])
def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
if len(finished_items) == 1:
return flt(outgoing_items_cost / finished_item_qty)
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
if row.t_warehouse and not row.s_warehouse])
def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
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"]:
for d in self.get("items"):
if (d.transfer_qty and (d.bom_no or d.t_warehouse)
and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)):
# Get raw materials cost from BOM if multiple material consumption entries
if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
if (self.work_order and self.purpose == "Manufacture"
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)
return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)
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.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 d.t_warehouse and total_basic_amount:
d.additional_cost = (flt(d.basic_amount) / total_basic_amount) * self.total_additional_costs
else:
d.additional_cost = 0
if self.purpose in ("Repack", "Manufacture"):
incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
else:
incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
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):
for d in self.get("items"):
@ -631,71 +633,115 @@ class StockEntry(StockController):
item_code = d.original_item or d.item_code
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):
"""validation: finished good quantity should be same as manufacturing quantity"""
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",
self.work_order, ["production_item", "qty"])
number_of_finished_items = 0
for d in self.get('items'):
if (self.purpose != "Send to Subcontractor" and d.bom_no
and flt(d.transfer_qty) > flt(self.fg_completed_qty) and d.item_code == production_item):
frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
format(d.idx, d.transfer_qty, self.fg_completed_qty))
if d.is_finished_item:
if d.item_code != production_item:
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
.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))
number_of_finished_items += 1
if self.work_order and self.purpose == "Manufacture" and d.t_warehouse:
items_with_target_warehouse.append(d.item_code)
if number_of_finished_items > 1:
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)
if self.fg_completed_qty > allowed_qty:
frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}")
.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):
sl_entries = []
finished_item_row = self.get_finished_item_row()
# make sl entries for source warehouse first, then do for target warehouse
for d in self.get('items'):
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'):
if cstr(d.t_warehouse):
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
# }))
# make sl entries for source warehouse first
self.get_sle_for_source_warehouse(sl_entries, finished_item_row)
# SLE for target warehouse
self.get_sle_for_target_warehouse(sl_entries, finished_item_row)
# reverse sl entries if cancel
if self.docstatus == 2:
sl_entries.reverse()
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):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
@ -740,7 +786,7 @@ class StockEntry(StockController):
"credit": -1 * amount # put it as negative credit instead of debit purposefully
}, item=d))
return gl_entries
return process_gl_map(gl_entries)
def update_work_order(self):
def _validate_work_order(pro_doc):
@ -989,6 +1035,7 @@ class StockEntry(StockController):
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"),
"is_finished_item": 1
}
}, bom_no = self.bom_no)
@ -1027,32 +1074,29 @@ class StockEntry(StockController):
for item in itervalues(item_dict):
item.from_warehouse = ""
item.is_scrap_item = 1
return item_dict
def get_unconsumed_raw_materials(self):
wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order},
fields=["item_code", "required_qty", "consumed_qty"]
fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"]
)
work_order_qty = wo.material_transferred_for_manufacturing or wo.qty
for item in wo_items:
qty = item.required_qty
item_account_details = get_item_defaults(item.item_code, self.company)
# Take into account consumption if there are any.
if self.purpose == 'Manufacture':
req_qty_each = flt(item.required_qty / wo.qty)
if (flt(item.consumed_qty) != 0):
remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each)
exhaust_qty = req_qty_each * wo.produced_qty
if remaining_qty > exhaust_qty :
if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1:
qty =0
else:
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else:
qty = req_qty_each * flt(self.fg_completed_qty)
wo_item_qty = item.transferred_qty or item.required_qty
req_qty_each = (
(flt(wo_item_qty) - flt(item.consumed_qty)) /
(flt(work_order_qty) - flt(wo.produced_qty))
)
qty = req_qty_each * flt(self.fg_completed_qty)
if qty > 0:
self.add_to_stock_entry_detail({
@ -1134,13 +1178,15 @@ class StockEntry(StockController):
else:
qty = req_qty_each * flt(self.fg_completed_qty)
elif backflushed_materials.get(item.item_code):
for d in backflushed_materials.get(item.item_code):
if d.get(item.warehouse):
if (qty > req_qty):
qty = (qty/trans_qty) * flt(self.fg_completed_qty)
if consumed_qty:
qty -= consumed_qty
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
qty = frappe.utils.ceil(qty)
@ -1241,6 +1287,8 @@ class StockEntry(StockController):
se_child.subcontracted_item = item_dict[d].get("main_item_code")
se_child.cost_center = (item_dict[d].get("cost_center") or
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",
"expense_account", "description", "item_name"]:

View File

@ -6,7 +6,6 @@ import frappe, unittest
import frappe.defaults
from frappe.utils import flt, nowdate, nowtime
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.stock_ledger import get_previous_sle
from frappe.permissions import add_user_permission, remove_user_permission
@ -32,7 +31,6 @@ def get_sle(**args):
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
set_perpetual_inventory(0)
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@ -213,7 +211,6 @@ class TestStockEntry(unittest.TestCase):
def test_repack_no_change_in_valuation(self):
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 Home Desktop 100", target="_Test Warehouse - _TC",
@ -235,8 +232,6 @@ class TestStockEntry(unittest.TestCase):
order by account desc""", repack.name, as_dict=1)
self.assertFalse(gl_entries)
set_perpetual_inventory(0, repack.company)
def test_repack_with_additional_costs(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
@ -474,7 +469,6 @@ class TestStockEntry(unittest.TestCase):
def test_warehouse_company_validation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
set_perpetual_inventory(0, company)
frappe.get_doc("User", "test2@example.com")\
.add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager")
frappe.set_user("test2@example.com")
@ -500,7 +494,7 @@ class TestStockEntry(unittest.TestCase):
st1 = frappe.copy_doc(test_records[0])
st1.company = "_Test Company 1"
set_perpetual_inventory(0, st1.company)
frappe.set_user("test@example.com")
st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
self.assertRaises(frappe.PermissionError, st1.insert)
@ -698,47 +692,54 @@ class TestStockEntry(unittest.TestCase):
repack.insert()
self.assertRaises(frappe.ValidationError, repack.submit)
def test_material_consumption(self):
from erpnext.manufacturing.doctype.work_order.work_order \
import make_stock_entry as _make_stock_entry
bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
"is_default": 1, "docstatus": 1})
# def test_material_consumption(self):
# frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
# frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
work_order = frappe.new_doc("Work Order")
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
})
work_order.insert()
work_order.submit()
# from erpnext.manufacturing.doctype.work_order.work_order \
# import make_stock_entry as _make_stock_entry
# bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
# "is_default": 1, "docstatus": 1})
make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
# work_order = frappe.new_doc("Work Order")
# 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 = {
'_Test Item': 10.0,
'_Test Item 2': 12.0,
'_Test Serialized Item With Series': 6.0
}
# make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
# make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
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)
# item_quantity = {
# '_Test Item': 2.0,
# '_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):
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].amount, 0)
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)
@ -759,8 +760,8 @@ class TestStockEntry(unittest.TestCase):
"company":"_Test Company with perpetual inventory",
"items":[
{
"item_code":"Basil Leaves",
"description":"Basil Leaves",
"item_code":"_Test Item",
"description":"_Test Item",
"qty": 1,
"basic_rate": 0,
"uom":"Nos",
@ -769,8 +770,8 @@ class TestStockEntry(unittest.TestCase):
"cost_center": "Main - TCP1"
},
{
"item_code":"Basil Leaves",
"description":"Basil Leaves",
"item_code":"_Test Item",
"description":"_Test Item",
"qty": 2,
"basic_rate": 0,
"uom":"Nos",

View File

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

View File

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

View File

@ -10,8 +10,10 @@ from frappe.model.document import Document
from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
from erpnext.accounts.utils import get_fiscal_year
from frappe.core.doctype.role.role import get_users
class StockFreezeError(frappe.ValidationError): pass
class BackDatedStockTransaction(frappe.ValidationError): pass
exclude_from_linked_with = True
@ -34,7 +36,6 @@ class StockLedgerEntry(Document):
self.validate_and_set_fiscal_year()
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
self.validate_future_posting()
def on_submit(self):
self.check_stock_frozen_date()
@ -48,7 +49,7 @@ class StockLedgerEntry(Document):
def calculate_batch_qty(self):
if self.batch_no:
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
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
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
if not self.batch_no:
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}):
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))
if item_det.has_variants:
@ -142,28 +143,28 @@ class StockLedgerEntry(Document):
is_group_warehouse(self.warehouse)
def validate_with_last_transaction_posting_time(self):
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]
authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions")
if authorized_role:
authorized_users = get_users(authorized_role)
if authorized_users and frappe.session.user not in authorized_users:
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):
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))
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),
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(
frappe.bold(self.item_code), frappe.bold(self.warehouse))
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))
msg += "<br><br>" + _("Please remove this item and try to submit again or update the posting time.")
frappe.throw(msg, 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"))
msg += "<br><br>" + _("Please contact any of the following users to {} this transaction.")
msg += "<br>" + "<br>".join(authorized_users)
frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
def on_doctype_update():
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 unittest
# test_records = frappe.get_test_records('Stock Ledger Entry')
from frappe.utils import today, add_days
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):
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):
self.update_stock_ledger()
self.make_gl_entries()
self.repost_future_sle_and_gle()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
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_gl_entries_on_cancel()
self.repost_future_sle_and_gle()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""

View File

@ -8,12 +8,11 @@ from __future__ import unicode_literals
import frappe, unittest
from frappe.utils import flt, nowdate, nowtime
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.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
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
class TestStockReconciliation(unittest.TestCase):
@ -29,16 +28,17 @@ class TestStockReconciliation(unittest.TestCase):
self._test_reco_sle_gle("Moving Average")
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')
# [[qty, valuation_rate, posting_date,
# posting_time, expected_stock_value, bin_qty, bin_valuation]]
input_data = [
[50, 1000],
[25, 900],
["", 1000],
[20, ""],
[0, ""]
[50, 1000, "2012-12-26", "12:00"],
[25, 900, "2012-12-26", "12:00"],
["", 1000, "2012-12-20", "12:05"],
[20, "", "2012-12-26", "12:05"],
[0, "", "2012-12-31", "12:10"]
]
for d in input_data:
@ -47,13 +47,13 @@ class TestStockReconciliation(unittest.TestCase):
last_sle = get_previous_sle({
"item_code": "_Test Item",
"warehouse": "Stores - TCP1",
"posting_date": nowdate(),
"posting_time": nowtime()
"posting_date": d[2],
"posting_time": d[3]
})
# submit stock reconciliation
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")
# check stock value
@ -81,10 +81,15 @@ class TestStockReconciliation(unittest.TestCase):
stock_reco.cancel()
se3.cancel()
se2.cancel()
se1.cancel()
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",
{"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,
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"]])
def test_stock_reco_for_serialized_item(self):
set_perpetual_inventory()
to_delete_records = []
to_delete_serial_nos = []
@ -148,8 +151,6 @@ class TestStockReconciliation(unittest.TestCase):
stock_doc.cancel()
def test_stock_reco_for_batch_item(self):
set_perpetual_inventory()
to_delete_records = []
to_delete_serial_nos = []
@ -196,15 +197,17 @@ class TestStockReconciliation(unittest.TestCase):
def insert_existing_sle(warehouse):
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)
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)
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)
return se1, se2, se3
def create_batch_or_serial_no_items():
create_warehouse("_Test Warehouse for Stock Reco1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
@ -256,6 +259,10 @@ def create_stock_reconciliation(**args):
return sr
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)
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",
"allow_from_dn",
"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_days",
"stock_auth_role",
@ -156,21 +158,20 @@
"label": "Notify by Email on Creation of Automatic Material Request"
},
{
"fieldname": "freeze_stock_entries",
"fieldtype": "Section Break",
"label": "Freeze Stock Entries"
},
{
"description": "No stock transactions can be created or modified before this date.",
"fieldname": "stock_frozen_upto",
"fieldtype": "Date",
"label": "Stock Frozen Upto"
},
{
"description": "Stock transactions that are older than the mentioned days cannot be modified.",
"fieldname": "stock_frozen_upto_days",
"fieldtype": "Int",
"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",
"fieldtype": "Link",
"label": "Role Allowed to Edit Frozen Stock",
@ -210,6 +211,22 @@
"fieldname": "allow_from_pr",
"fieldtype": "Check",
"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",
@ -217,7 +234,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2020-11-23 15:26:54.225608",
"modified": "2020-11-23 22:26:54.225608",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",

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