mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-27 02:28:30 +00:00
Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-29740
This commit is contained in:
@@ -121,6 +121,7 @@ def get_booking_dates(doc, item, posting_date=None):
|
||||
prev_gl_entry = frappe.db.sql('''
|
||||
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
|
||||
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
order by posting_date desc limit 1
|
||||
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
|
||||
@@ -228,6 +229,7 @@ def get_already_booked_amount(doc, item):
|
||||
gl_entries_details = frappe.db.sql('''
|
||||
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
|
||||
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
group by voucher_detail_no
|
||||
'''.format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
@@ -283,7 +285,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
return
|
||||
|
||||
# check if books nor frozen till endate:
|
||||
if getdate(end_date) >= getdate(accounts_frozen_upto):
|
||||
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
|
||||
if via_journal_entry:
|
||||
|
||||
@@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
frappe.scrub(row.party_type): row.party,
|
||||
"is_pos": 0,
|
||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||
"update_stock": 0,
|
||||
"update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
|
||||
"invoice_number": row.invoice_number,
|
||||
"disable_rounded_total": 1
|
||||
})
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.cache_manager import clear_doctype_cache
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
@@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
|
||||
|
||||
class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
def setUp(self):
|
||||
class TestOpeningInvoiceCreationTool(ERPNextTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
make_company()
|
||||
create_dimension()
|
||||
return super().setUpClass()
|
||||
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
@@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
return doc.make_invoices()
|
||||
|
||||
def test_opening_sales_invoice_creation(self):
|
||||
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
|
||||
try:
|
||||
invoices = self.make_invoices(company="_Test Opening Invoice Company")
|
||||
invoices = self.make_invoices(company="_Test Opening Invoice Company")
|
||||
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
|
||||
si = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
si = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
|
||||
# Check if update stock is not enabled
|
||||
self.assertEqual(si.update_stock, 0)
|
||||
|
||||
finally:
|
||||
property_setter.delete()
|
||||
clear_doctype_cache("Sales Invoice")
|
||||
# Check if update stock is not enabled
|
||||
self.assertEqual(si.update_stock, 0)
|
||||
|
||||
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
|
||||
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
|
||||
|
||||
@@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
|
||||
|
||||
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
|
||||
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
|
||||
(frm.doc.paid_from_account_currency != company_currency));
|
||||
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
|
||||
(frm.doc.paid_to_account_currency != company_currency));
|
||||
} else {
|
||||
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
|
||||
(frm.doc.paid_from_account_currency != company_currency));
|
||||
}
|
||||
|
||||
frm.toggle_display("base_received_amount", (
|
||||
frm.doc.paid_to_account_currency != company_currency
|
||||
@@ -232,7 +238,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
||||
|
||||
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
|
||||
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
|
||||
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
|
||||
"base_total_taxes_and_charges"], company_currency);
|
||||
|
||||
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
|
||||
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
|
||||
@@ -341,6 +348,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
frm.set_party_account_based_on_party = true;
|
||||
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
|
||||
args: {
|
||||
@@ -374,7 +383,11 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if (r.message.bank_account) {
|
||||
frm.set_value("bank_account", r.message.bank_account);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
|
||||
frm.doc.paid_from_account_currency, company_currency),
|
||||
() => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
|
||||
frm.doc.paid_to_account_currency, company_currency)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -478,14 +491,14 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
paid_from_account_currency: function(frm) {
|
||||
if(!frm.doc.paid_from_account_currency) return;
|
||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from){
|
||||
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
@@ -505,8 +518,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
paid_to_account_currency: function(frm) {
|
||||
if(!frm.doc.paid_to_account_currency) return;
|
||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
|
||||
frm.doc.paid_to_account_currency, company_currency);
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
"tax_withholding_category",
|
||||
"section_break_56",
|
||||
"taxes",
|
||||
"section_break_60",
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_61",
|
||||
"total_taxes_and_charges",
|
||||
"deductions_or_loss_section",
|
||||
"deductions",
|
||||
@@ -715,12 +717,21 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Paid To Account Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_61",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-24 18:58:24.919764",
|
||||
"modified": "2022-02-23 20:08:39.559814",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
@@ -763,6 +774,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -946,8 +946,12 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
tax.base_total = tax.total * self.source_exchange_rate
|
||||
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
|
||||
if self.payment_type == 'Pay':
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
else:
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
|
||||
if self.get('taxes'):
|
||||
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
|
||||
@@ -1078,7 +1082,7 @@ def get_outstanding_reference_documents(args):
|
||||
if d.voucher_type in ("Purchase Invoice"):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get all SO / PO which are not fully billed or aginst which full advance not paid
|
||||
# Get all SO / PO which are not fully billed or against which full advance not paid
|
||||
orders_to_be_billed = []
|
||||
if (args.get("party_type") != "Student"):
|
||||
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
|
||||
|
||||
@@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase):
|
||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
|
||||
|
||||
def test_multi_currency_payment_entry_with_taxes(self):
|
||||
payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC',
|
||||
save=True)
|
||||
payment_entry.append('taxes', {
|
||||
'account_head': '_Test Account Service Tax - _TC',
|
||||
'charge_type': 'Actual',
|
||||
'tax_amount': 10,
|
||||
'add_deduct_tax': 'Add',
|
||||
'description': 'Test'
|
||||
})
|
||||
|
||||
payment_entry.save()
|
||||
self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
|
||||
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc('Payment Entry')
|
||||
payment_entry.company = args.get('company') or '_Test Company'
|
||||
payment_entry.payment_type = args.get('payment_type') or 'Pay'
|
||||
payment_entry.party_type = args.get('party_type') or 'Supplier'
|
||||
payment_entry.party = args.get('party') or '_Test Supplier'
|
||||
payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
|
||||
payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
|
||||
payment_entry.paid_amount = args.get('paid_amount') or 1000
|
||||
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
payment_entry.set_exchange_rate()
|
||||
payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
|
||||
payment_entry.reference_no = 'Test001'
|
||||
payment_entry.reference_date = nowdate()
|
||||
|
||||
if args.get('save'):
|
||||
payment_entry.save()
|
||||
if args.get('submit'):
|
||||
payment_entry.submit()
|
||||
|
||||
return payment_entry
|
||||
|
||||
def create_payment_terms_template():
|
||||
|
||||
create_payment_term('Basic Amount Receivable')
|
||||
|
||||
@@ -439,7 +439,6 @@ class POSInvoice(SalesInvoice):
|
||||
self.paid_amount = 0
|
||||
|
||||
def set_account_for_mode_of_payment(self):
|
||||
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
|
||||
for pay in self.payments:
|
||||
if not pay.account:
|
||||
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
|
||||
|
||||
@@ -85,20 +85,12 @@ class POSInvoiceMergeLog(Document):
|
||||
sales_invoice.set_posting_time = 1
|
||||
sales_invoice.posting_date = getdate(self.posting_date)
|
||||
sales_invoice.save()
|
||||
self.write_off_fractional_amount(sales_invoice, data)
|
||||
sales_invoice.submit()
|
||||
|
||||
self.consolidated_invoice = sales_invoice.name
|
||||
|
||||
return sales_invoice.name
|
||||
|
||||
def write_off_fractional_amount(self, invoice, data):
|
||||
pos_invoice_grand_total = sum(d.grand_total for d in data)
|
||||
|
||||
if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
|
||||
invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
|
||||
invoice.save()
|
||||
|
||||
def process_merging_into_credit_note(self, data):
|
||||
credit_note = self.get_new_sales_invoice()
|
||||
credit_note.is_return = 1
|
||||
@@ -111,7 +103,6 @@ class POSInvoiceMergeLog(Document):
|
||||
# TODO: return could be against multiple sales invoice which could also have been consolidated?
|
||||
# credit_note.return_against = self.consolidated_invoice
|
||||
credit_note.save()
|
||||
self.write_off_fractional_amount(credit_note, data)
|
||||
credit_note.submit()
|
||||
|
||||
self.consolidated_credit_note = credit_note.name
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
|
||||
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
@@ -280,3 +281,100 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
@change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3})
|
||||
def test_consolidation_round_off_error_3(self):
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
make_stock_entry(
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
item_code="_Test Item",
|
||||
rate=8000,
|
||||
qty=10,
|
||||
)
|
||||
init_user_and_profile()
|
||||
|
||||
item_rates = [69, 59, 29]
|
||||
for i in [1, 2]:
|
||||
inv = create_pos_invoice(is_return=1, do_not_save=1)
|
||||
inv.items = []
|
||||
for rate in item_rates:
|
||||
inv.append("items", {
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": -1,
|
||||
"rate": rate,
|
||||
"income_account": "Sales - _TC",
|
||||
"expense_account": "Cost of Goods Sold - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
})
|
||||
inv.append("taxes", {
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"description": "VAT",
|
||||
"doctype": "Sales Taxes and Charges",
|
||||
"rate": 15,
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
inv.payments = []
|
||||
inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157
|
||||
})
|
||||
inv.paid_amount = -157
|
||||
inv.save()
|
||||
inv.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.status, 'Return')
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
def test_consolidation_rounding_adjustment(self):
|
||||
'''
|
||||
Test if the rounding adjustment is calculated correctly
|
||||
'''
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
|
||||
try:
|
||||
make_stock_entry(
|
||||
to_warehouse="_Test Warehouse - _TC",
|
||||
item_code="_Test Item",
|
||||
rate=8000,
|
||||
qty=10,
|
||||
)
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True)
|
||||
inv.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70
|
||||
})
|
||||
inv.insert()
|
||||
inv.submit()
|
||||
|
||||
inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True)
|
||||
inv2.append('payments', {
|
||||
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60
|
||||
})
|
||||
inv2.insert()
|
||||
inv2.submit()
|
||||
|
||||
consolidate_pos_invoices()
|
||||
|
||||
inv.load_from_db()
|
||||
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
|
||||
self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
|
||||
|
||||
finally:
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
frappe.db.sql("delete from `tabPOS Invoice`")
|
||||
@@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True):
|
||||
'to_date': doc.to_date,
|
||||
'company': doc.company,
|
||||
'finance_book': doc.finance_book if doc.finance_book else None,
|
||||
'account': doc.account if doc.account else None,
|
||||
'account': [doc.account] if doc.account else None,
|
||||
'party_type': 'Customer',
|
||||
'party': [entry.customer],
|
||||
'presentation_currency': presentation_currency,
|
||||
|
||||
@@ -272,6 +272,9 @@ class SalesInvoice(SellingController):
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def validate_pos_return(self):
|
||||
if self.is_consolidated:
|
||||
# pos return is already validated in pos invoice
|
||||
return
|
||||
|
||||
if self.is_pos and self.is_return:
|
||||
total_amount_in_payments = 0
|
||||
|
||||
@@ -1603,6 +1603,56 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||
|
||||
def test_rounding_adjustment_3(self):
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.items = []
|
||||
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
|
||||
si.append("items", {
|
||||
"item_code": "_Test Item",
|
||||
"gst_hsn_code": "999800",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": d[1],
|
||||
"rate": d[0],
|
||||
"income_account": "Sales - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
})
|
||||
for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
|
||||
si.append("taxes", {
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": tax_account,
|
||||
"description": tax_account,
|
||||
"rate": 6,
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"included_in_print_rate": 1
|
||||
})
|
||||
si.save()
|
||||
si.submit()
|
||||
self.assertEqual(si.net_total, 4007.16)
|
||||
self.assertEqual(si.grand_total, 4488.02)
|
||||
self.assertEqual(si.total_taxes_and_charges, 480.86)
|
||||
self.assertEqual(si.rounding_adjustment, -0.02)
|
||||
|
||||
expected_values = dict((d[0], d) for d in [
|
||||
[si.debit_to, 4488.0, 0.0],
|
||||
["_Test Account Service Tax - _TC", 0.0, 240.43],
|
||||
["_Test Account VAT - _TC", 0.0, 240.43],
|
||||
["Sales - _TC", 0.0, 4007.15],
|
||||
["Round Off - _TC", 0.01, 0]
|
||||
])
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||
order by account asc""", si.name, as_dict=1)
|
||||
|
||||
debit_credit_diff = 0
|
||||
for gle in 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)
|
||||
debit_credit_diff += (gle.debit - gle.credit)
|
||||
|
||||
self.assertEqual(debit_credit_diff, 0)
|
||||
|
||||
def test_sales_invoice_with_shipping_rule(self):
|
||||
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
|
||||
|
||||
@@ -2354,14 +2404,22 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
|
||||
def test_sales_commission(self):
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si = frappe.copy_doc(test_records[2])
|
||||
|
||||
frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1)
|
||||
frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0)
|
||||
|
||||
item = copy.deepcopy(si.get('items')[0])
|
||||
item.update({
|
||||
"qty": 1,
|
||||
"rate": 500,
|
||||
"grant_commission": 1
|
||||
})
|
||||
si.append("items", item)
|
||||
|
||||
item = copy.deepcopy(si.get('items')[1])
|
||||
item.update({
|
||||
"qty": 1,
|
||||
"rate": 500,
|
||||
})
|
||||
|
||||
# Test valid values
|
||||
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
|
||||
|
||||
@@ -832,6 +832,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
@@ -841,7 +842,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-05 12:24:54.968907",
|
||||
"modified": "2022-02-24 14:41:36.392560",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -851,3 +852,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
validate_taxes_and_charges(tax)
|
||||
validate_account_head(tax, doc)
|
||||
validate_account_head(tax.idx, tax.account_head, doc.company)
|
||||
validate_cost_center(tax, doc)
|
||||
validate_inclusive_tax(tax, doc)
|
||||
|
||||
@@ -55,5 +55,8 @@ def validate_disabled(doc):
|
||||
frappe.throw(_("Disabled template must not be default template"))
|
||||
|
||||
def validate_for_tax_category(doc):
|
||||
if not doc.tax_category:
|
||||
return
|
||||
|
||||
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
|
||||
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
|
||||
|
||||
@@ -221,7 +221,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
debit_credit_diff += flt(d.credit)
|
||||
round_off_account_exists = True
|
||||
|
||||
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
|
||||
if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
|
||||
gl_map.remove(round_off_gle)
|
||||
return
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
|
||||
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
|
||||
|
||||
def validate_party_accounts(doc):
|
||||
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
companies = []
|
||||
|
||||
for account in doc.get("accounts"):
|
||||
@@ -331,6 +331,9 @@ def validate_party_accounts(doc):
|
||||
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
|
||||
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
|
||||
|
||||
# validate if account is mapped for same company
|
||||
validate_account_head(account.idx, account.account, account.company)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
|
||||
|
||||
@@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
|
||||
'target_ref_field': 'stock_qty',
|
||||
'source_field': 'stock_qty'
|
||||
})
|
||||
self.status_updater.append({
|
||||
'source_dt': 'Purchase Order Item',
|
||||
'target_dt': 'Packed Item',
|
||||
'target_field': 'ordered_qty',
|
||||
'target_parent_dt': 'Sales Order',
|
||||
'target_parent_field': '',
|
||||
'join_field': 'sales_order_packed_item',
|
||||
'target_ref_field': 'qty',
|
||||
'source_field': 'stock_qty'
|
||||
})
|
||||
|
||||
def update_delivered_qty_in_sales_order(self):
|
||||
"""Update delivered qty in Sales Order for drop ship"""
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -27,7 +27,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_make_purchase_receipt(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"material_request_item",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
"sales_order_packed_item",
|
||||
"supplier_quotation",
|
||||
"supplier_quotation_item",
|
||||
"col_break5",
|
||||
@@ -837,21 +838,30 @@
|
||||
"label": "Product Bundle",
|
||||
"options": "Product Bundle",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order_packed_item",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sales Order Packed Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 20:06:26.712097",
|
||||
"modified": "2022-02-02 13:10:18.398976",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "item_name",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
|
||||
@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
|
||||
|
||||
|
||||
class TestRequestforQuotation(unittest.TestCase):
|
||||
class TestRequestforQuotation(FrappeTestCase):
|
||||
def test_quote_status(self):
|
||||
rfq = make_request_for_quotation()
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.party import get_due_date
|
||||
from erpnext.exceptions import PartyDisabled
|
||||
@@ -13,7 +13,7 @@ test_dependencies = ['Payment Term', 'Payment Terms Template']
|
||||
test_records = frappe.get_test_records('Supplier')
|
||||
|
||||
|
||||
class TestSupplier(unittest.TestCase):
|
||||
class TestSupplier(FrappeTestCase):
|
||||
def test_get_supplier_group_details(self):
|
||||
doc = frappe.new_doc("Supplier Group")
|
||||
doc.supplier_group_name = "_Testing Supplier Group"
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_make_purchase_order(self):
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestSupplierScorecard(unittest.TestCase):
|
||||
class TestSupplierScorecard(FrappeTestCase):
|
||||
|
||||
def test_create_scorecard(self):
|
||||
doc = make_supplier_scorecard().insert()
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestSupplierScorecardCriteria(unittest.TestCase):
|
||||
class TestSupplierScorecardCriteria(FrappeTestCase):
|
||||
def test_variables_exist(self):
|
||||
delete_test_scorecards()
|
||||
for d in test_good_criteria:
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import (
|
||||
VariablePathNotFound,
|
||||
)
|
||||
|
||||
|
||||
class TestSupplierScorecardVariable(unittest.TestCase):
|
||||
class TestSupplierScorecardVariable(FrappeTestCase):
|
||||
def test_variable_exist(self):
|
||||
for d in test_existing_variables:
|
||||
my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name"))
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.report.procurement_tracker.procurement_tracker import execute
|
||||
@@ -14,7 +14,7 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
|
||||
class TestProcurementTracker(unittest.TestCase):
|
||||
class TestProcurementTracker(FrappeTestCase):
|
||||
def test_result_for_procurement_tracker(self):
|
||||
filters = {
|
||||
'company': '_Test Procurement Company',
|
||||
|
||||
@@ -6,6 +6,7 @@ import copy
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Coalesce, Sum
|
||||
from frappe.utils import date_diff, flt, getdate
|
||||
|
||||
|
||||
@@ -16,12 +17,9 @@ def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
|
||||
columns = get_columns(filters)
|
||||
conditions = get_conditions(filters)
|
||||
data = get_data(filters)
|
||||
|
||||
#get queried data
|
||||
data = get_data(filters, conditions)
|
||||
|
||||
#prepare data for report and chart views
|
||||
# prepare data for report and chart views
|
||||
data, chart_data = prepare_data(data, filters)
|
||||
|
||||
return columns, data, None, chart_data
|
||||
@@ -34,53 +32,70 @@ def validate_filters(filters):
|
||||
elif date_diff(to_date, from_date) < 0:
|
||||
frappe.throw(_("To Date cannot be before From Date."))
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = ''
|
||||
def get_data(filters):
|
||||
mr = frappe.qb.DocType("Material Request")
|
||||
mr_item = frappe.qb.DocType("Material Request Item")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(mr)
|
||||
.join(mr_item).on(mr_item.parent == mr.name)
|
||||
.select(
|
||||
mr.name.as_("material_request"),
|
||||
mr.transaction_date.as_("date"),
|
||||
mr_item.schedule_date.as_("required_date"),
|
||||
mr_item.item_code.as_("item_code"),
|
||||
Sum(Coalesce(mr_item.stock_qty, 0)).as_("qty"),
|
||||
Coalesce(mr_item.stock_uom, '').as_("uom"),
|
||||
Sum(Coalesce(mr_item.ordered_qty, 0)).as_("ordered_qty"),
|
||||
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||
(
|
||||
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.received_qty, 0))
|
||||
).as_("qty_to_receive"),
|
||||
Sum(Coalesce(mr_item.received_qty, 0)).as_("received_qty"),
|
||||
(
|
||||
Sum(Coalesce(mr_item.stock_qty, 0)) - Sum(Coalesce(mr_item.ordered_qty, 0))
|
||||
).as_("qty_to_order"),
|
||||
mr_item.item_name,
|
||||
mr_item.description,
|
||||
mr.company
|
||||
).where(
|
||||
(mr.material_request_type == "Purchase")
|
||||
& (mr.docstatus == 1)
|
||||
& (mr.status != "Stopped")
|
||||
& (mr.per_received < 100)
|
||||
)
|
||||
)
|
||||
|
||||
query = get_conditions(filters, query, mr, mr_item) # add conditional conditions
|
||||
|
||||
query = (
|
||||
query.groupby(
|
||||
mr.name, mr_item.item_code
|
||||
).orderby(
|
||||
mr.transaction_date, mr.schedule_date
|
||||
)
|
||||
)
|
||||
data = query.run(as_dict=True)
|
||||
return data
|
||||
|
||||
def get_conditions(filters, query, mr, mr_item):
|
||||
if filters.get("from_date") and filters.get("to_date"):
|
||||
conditions += " and mr.transaction_date between '{0}' and '{1}'".format(filters.get("from_date"),filters.get("to_date"))
|
||||
|
||||
query = (
|
||||
query.where(
|
||||
(mr.transaction_date >= filters.get("from_date"))
|
||||
& (mr.transaction_date <= filters.get("to_date"))
|
||||
)
|
||||
)
|
||||
if filters.get("company"):
|
||||
conditions += " and mr.company = '{0}'".format(filters.get("company"))
|
||||
query = query.where(mr.company == filters.get("company"))
|
||||
|
||||
if filters.get("material_request"):
|
||||
conditions += " and mr.name = '{0}'".format(filters.get("material_request"))
|
||||
query = query.where(mr.name == filters.get("material_request"))
|
||||
|
||||
if filters.get("item_code"):
|
||||
conditions += " and mr_item.item_code = '{0}'".format(filters.get("item_code"))
|
||||
query = query.where(mr_item.item_code == filters.get("item_code"))
|
||||
|
||||
return conditions
|
||||
|
||||
def get_data(filters, conditions):
|
||||
data = frappe.db.sql("""
|
||||
select
|
||||
mr.name as material_request,
|
||||
mr.transaction_date as date,
|
||||
mr_item.schedule_date as required_date,
|
||||
mr_item.item_code as item_code,
|
||||
sum(ifnull(mr_item.stock_qty, 0)) as qty,
|
||||
ifnull(mr_item.stock_uom, '') as uom,
|
||||
sum(ifnull(mr_item.ordered_qty, 0)) as ordered_qty,
|
||||
sum(ifnull(mr_item.received_qty, 0)) as received_qty,
|
||||
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.received_qty, 0))) as qty_to_receive,
|
||||
(sum(ifnull(mr_item.stock_qty, 0)) - sum(ifnull(mr_item.ordered_qty, 0))) as qty_to_order,
|
||||
mr_item.item_name as item_name,
|
||||
mr_item.description as "description",
|
||||
mr.company as company
|
||||
from
|
||||
`tabMaterial Request` mr, `tabMaterial Request Item` mr_item
|
||||
where
|
||||
mr_item.parent = mr.name
|
||||
and mr.material_request_type = "Purchase"
|
||||
and mr.docstatus = 1
|
||||
and mr.status != "Stopped"
|
||||
{conditions}
|
||||
group by mr.name, mr_item.item_code
|
||||
having
|
||||
sum(ifnull(mr_item.ordered_qty, 0)) < sum(ifnull(mr_item.stock_qty, 0))
|
||||
order by mr.transaction_date, mr.schedule_date""".format(conditions=conditions), as_dict=1)
|
||||
|
||||
return data
|
||||
return query
|
||||
|
||||
def update_qty_columns(row_to_update, data_row):
|
||||
fields = ["qty", "ordered_qty", "received_qty", "qty_to_receive", "qty_to_order"]
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.report.requested_items_to_order_and_receive.requested_items_to_order_and_receive import (
|
||||
get_data,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
|
||||
|
||||
class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
|
||||
def setUp(self) -> None:
|
||||
create_item("Test MR Report Item")
|
||||
self.setup_material_request() # to order and receive
|
||||
self.setup_material_request(order=True) # to receive (ordered)
|
||||
self.setup_material_request(order=True, receive=True) # complete (ordered & received)
|
||||
|
||||
self.filters = frappe._dict(
|
||||
company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
|
||||
item_code="Test MR Report Item"
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_date_range(self):
|
||||
data = get_data(self.filters)
|
||||
self.assertEqual(len(data), 2) # MRs today should be fetched
|
||||
|
||||
self.filters.from_date = add_days(today(), 1)
|
||||
data = get_data(self.filters)
|
||||
self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow
|
||||
|
||||
def test_ordered_received_material_requests(self):
|
||||
data = get_data(self.filters)
|
||||
|
||||
# from the 3 MRs made, only 2 (to receive) should be fetched
|
||||
self.assertEqual(len(data), 2)
|
||||
self.assertEqual(data[0].ordered_qty, 0.0)
|
||||
self.assertEqual(data[1].ordered_qty, 57.0)
|
||||
|
||||
def setup_material_request(self, order=False, receive=False):
|
||||
po = None
|
||||
test_records = frappe.get_test_records('Material Request')
|
||||
|
||||
mr = frappe.copy_doc(test_records[0])
|
||||
mr.transaction_date = today()
|
||||
mr.schedule_date = add_days(today(), 1)
|
||||
for row in mr.items:
|
||||
row.item_code = "Test MR Report Item"
|
||||
row.item_name = "Test MR Report Item"
|
||||
row.description = "Test MR Report Item"
|
||||
row.uom = "Nos"
|
||||
row.schedule_date = add_days(today(), 1)
|
||||
mr.submit()
|
||||
|
||||
if order or receive:
|
||||
po = make_purchase_order(mr.name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.submit()
|
||||
if receive:
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.submit()
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# Compiled at: 2019-05-06 09:51:46
|
||||
# Decompiled by https://python-decompiler.com
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -15,7 +15,7 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestSubcontractedItemToBeReceived(unittest.TestCase):
|
||||
class TestSubcontractedItemToBeReceived(FrappeTestCase):
|
||||
|
||||
def test_pending_and_received_qty(self):
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
# Decompiled by https://python-decompiler.com
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -16,7 +16,7 @@ from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcont
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
|
||||
class TestSubcontractedItemToBeTransferred(FrappeTestCase):
|
||||
|
||||
def test_pending_and_transferred_qty(self):
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
@@ -1567,13 +1567,12 @@ def validate_taxes_and_charges(tax):
|
||||
tax.rate = None
|
||||
|
||||
|
||||
def validate_account_head(tax, doc):
|
||||
company = frappe.get_cached_value('Account',
|
||||
tax.account_head, 'company')
|
||||
def validate_account_head(idx, account, company):
|
||||
account_company = frappe.get_cached_value('Account', account, 'company')
|
||||
|
||||
if company != doc.company:
|
||||
if account_company != company:
|
||||
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
|
||||
.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
|
||||
.format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
|
||||
|
||||
|
||||
def validate_cost_center(tax, doc):
|
||||
|
||||
@@ -507,13 +507,41 @@ class StockController(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"company": self.company
|
||||
})
|
||||
if future_sle_exists(args):
|
||||
|
||||
if future_sle_exists(args) or repost_required_for_queue(self):
|
||||
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
|
||||
if item_based_reposting:
|
||||
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
else:
|
||||
create_repost_item_valuation_entry(args)
|
||||
|
||||
def repost_required_for_queue(doc: StockController) -> bool:
|
||||
"""check if stock document contains repeated item-warehouse with queue based valuation.
|
||||
|
||||
if queue exists for repeated items then SLEs need to reprocessed in background again.
|
||||
"""
|
||||
|
||||
consuming_sles = frappe.db.get_all("Stock Ledger Entry",
|
||||
filters={
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"actual_qty": ("<", 0),
|
||||
"is_cancelled": 0
|
||||
},
|
||||
fields=["item_code", "warehouse", "stock_queue"]
|
||||
)
|
||||
item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
|
||||
|
||||
unique_item_warehouses = set(item_warehouses)
|
||||
|
||||
if len(unique_item_warehouses) == len(item_warehouses):
|
||||
return False
|
||||
|
||||
for sle in consuming_sles:
|
||||
if sle.stock_queue != "[]": # using FIFO/LIFO valuation
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
|
||||
@@ -363,8 +363,6 @@ class Subcontracting():
|
||||
return
|
||||
|
||||
for row in self.get(self.raw_material_table):
|
||||
self.__validate_consumed_qty(row)
|
||||
|
||||
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
|
||||
if not self.__transferred_items or not self.__transferred_items.get(key):
|
||||
return
|
||||
@@ -372,12 +370,6 @@ class Subcontracting():
|
||||
self.__validate_batch_no(row, key)
|
||||
self.__validate_serial_no(row, key)
|
||||
|
||||
def __validate_consumed_qty(self, row):
|
||||
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
|
||||
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
|
||||
|
||||
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
|
||||
|
||||
def __validate_batch_no(self, row, key):
|
||||
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
|
||||
link = get_link_to_form('Purchase Order', row.purchase_order)
|
||||
|
||||
@@ -113,17 +113,24 @@ class calculate_taxes_and_totals(object):
|
||||
for item in self.doc.get("items"):
|
||||
self.doc.round_floats_in(item)
|
||||
|
||||
if not item.rate:
|
||||
item.rate = item.price_list_rate
|
||||
|
||||
if item.discount_percentage == 100:
|
||||
item.rate = 0.0
|
||||
elif item.price_list_rate:
|
||||
if not item.rate or (item.pricing_rules and item.discount_percentage > 0):
|
||||
if item.pricing_rules or abs(item.discount_percentage) > 0:
|
||||
item.rate = flt(item.price_list_rate *
|
||||
(1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
||||
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
|
||||
elif item.discount_amount and item.pricing_rules:
|
||||
|
||||
if abs(item.discount_percentage) > 0:
|
||||
item.discount_amount = item.price_list_rate * (item.discount_percentage / 100.0)
|
||||
|
||||
elif item.discount_amount or item.pricing_rules:
|
||||
item.rate = item.price_list_rate - item.discount_amount
|
||||
|
||||
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
|
||||
if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item',
|
||||
'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
|
||||
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
|
||||
if flt(item.rate_with_margin) > 0:
|
||||
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
|
||||
@@ -270,7 +277,8 @@ class calculate_taxes_and_totals(object):
|
||||
shipping_rule.apply(self.doc)
|
||||
|
||||
def calculate_taxes(self):
|
||||
if not self.doc.get('is_consolidated'):
|
||||
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
|
||||
if not rounding_adjustment_computed:
|
||||
self.doc.rounding_adjustment = 0
|
||||
|
||||
# maintain actual tax rate based on idx
|
||||
@@ -326,7 +334,7 @@ class calculate_taxes_and_totals(object):
|
||||
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
|
||||
and self.doc.discount_amount \
|
||||
and self.doc.apply_discount_on == "Grand Total" \
|
||||
and not self.doc.get('is_consolidated'):
|
||||
and not rounding_adjustment_computed:
|
||||
self.doc.rounding_adjustment = flt(self.doc.grand_total
|
||||
- flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
@@ -465,20 +473,22 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.total_net_weight += d.total_weight
|
||||
|
||||
def set_rounded_total(self):
|
||||
if not self.doc.get('is_consolidated'):
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
return
|
||||
if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'):
|
||||
return
|
||||
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
||||
self.doc.currency, self.doc.precision("rounded_total"))
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
return
|
||||
|
||||
#if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
||||
self.doc.currency, self.doc.precision("rounded_total"))
|
||||
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
#if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
|
||||
def _cleanup(self):
|
||||
if not self.doc.get('is_consolidated'):
|
||||
|
||||
@@ -265,7 +265,7 @@ class ProductQuery:
|
||||
customer = get_customer(silent=True)
|
||||
if customer:
|
||||
quotation = frappe.get_all("Quotation", fields=["name"], filters=
|
||||
{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
{"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
order_by="modified desc", limit_page_length=1)
|
||||
if quotation:
|
||||
items = frappe.get_all(
|
||||
@@ -299,4 +299,4 @@ class ProductQuery:
|
||||
# slice results manually
|
||||
result[:self.page_length]
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
@@ -311,7 +311,7 @@ def _get_cart_quotation(party=None):
|
||||
party = get_party()
|
||||
|
||||
quotation = frappe.get_all("Quotation", fields=["name"], filters=
|
||||
{"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
{"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
order_by="modified desc", limit_page_length=1)
|
||||
|
||||
if quotation:
|
||||
|
||||
@@ -56,13 +56,19 @@ class TestShoppingCart(unittest.TestCase):
|
||||
return quotation
|
||||
|
||||
def test_get_cart_customer(self):
|
||||
self.login_as_customer()
|
||||
def validate_quotation():
|
||||
# test if quotation with customer is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(quotation.party_name, "_Test Customer")
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
return quotation
|
||||
|
||||
# test if quotation with customer is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(quotation.party_name, "_Test Customer")
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer")
|
||||
validate_quotation()
|
||||
|
||||
self.login_as_customer()
|
||||
quotation = validate_quotation()
|
||||
|
||||
return quotation
|
||||
|
||||
@@ -253,10 +259,9 @@ class TestShoppingCart(unittest.TestCase):
|
||||
self.create_user_if_not_exists("test_cart_user@example.com")
|
||||
frappe.set_user("test_cart_user@example.com")
|
||||
|
||||
def login_as_customer(self):
|
||||
self.create_user_if_not_exists("test_contact_customer@example.com",
|
||||
"_Test Contact For _Test Customer")
|
||||
frappe.set_user("test_contact_customer@example.com")
|
||||
def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"):
|
||||
self.create_user_if_not_exists(email, name)
|
||||
frappe.set_user(email)
|
||||
|
||||
def clear_existing_quotations(self):
|
||||
quotations = frappe.get_all("Quotation", filters={
|
||||
|
||||
@@ -174,16 +174,22 @@ def get_month_map():
|
||||
def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
import calendar
|
||||
month_map = get_month_map()
|
||||
|
||||
today = get_datetime()
|
||||
|
||||
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(1, calendar.monthrange(today.year, month_map[month])[1] + 1)]
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", employee, ["date_of_joining", "relieving_date"])
|
||||
start_day = 1
|
||||
end_day = calendar.monthrange(today.year, month_map[month])[1] + 1
|
||||
|
||||
length = len(dates_of_month)
|
||||
month_start, month_end = dates_of_month[0], dates_of_month[length-1]
|
||||
if joining_date and joining_date.month == month_map[month]:
|
||||
start_day = joining_date.day
|
||||
|
||||
if relieving_date and relieving_date.month == month_map[month]:
|
||||
end_day = relieving_date.day + 1
|
||||
|
||||
records = frappe.get_all("Attendance", fields = ['attendance_date', 'employee'] , filters = [
|
||||
dates_of_month = ['{}-{}-{}'.format(today.year, month_map[month], r) for r in range(start_day, end_day)]
|
||||
month_start, month_end = dates_of_month[0], dates_of_month[-1]
|
||||
|
||||
records = frappe.get_all("Attendance", fields=['attendance_date', 'employee'], filters=[
|
||||
["attendance_date", ">=", month_start],
|
||||
["attendance_date", "<=", month_end],
|
||||
["employee", "=", employee],
|
||||
@@ -200,7 +206,7 @@ def get_unmarked_days(employee, month, exclude_holidays=0):
|
||||
|
||||
for date in dates_of_month:
|
||||
date_time = get_datetime(date)
|
||||
if today.day == date_time.day and today.month == date_time.month:
|
||||
if today.day <= date_time.day and today.month <= date_time.month:
|
||||
break
|
||||
if date_time not in marked_days:
|
||||
unmarked_days.append(date)
|
||||
|
||||
@@ -4,17 +4,104 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils import add_days, get_first_day, getdate, nowdate
|
||||
|
||||
from erpnext.hr.doctype.attendance.attendance import (
|
||||
get_month_map,
|
||||
get_unmarked_days,
|
||||
mark_attendance,
|
||||
)
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
|
||||
|
||||
test_records = frappe.get_test_records('Attendance')
|
||||
|
||||
class TestAttendance(unittest.TestCase):
|
||||
def test_mark_absent(self):
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
employee = make_employee("test_mark_absent@example.com")
|
||||
date = nowdate()
|
||||
frappe.db.delete('Attendance', {'employee':employee, 'attendance_date':date})
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
attendance = mark_attendance(employee, date, 'Absent')
|
||||
fetch_attendance = frappe.get_value('Attendance', {'employee':employee, 'attendance_date':date, 'status':'Absent'})
|
||||
self.assertEqual(attendance, fetch_attendance)
|
||||
|
||||
def test_unmarked_days(self):
|
||||
first_day = get_first_day(getdate())
|
||||
|
||||
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
|
||||
frappe.db.delete('Attendance', {'employee': employee})
|
||||
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
holiday_list = make_holiday_list()
|
||||
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(holiday_list)
|
||||
mark_attendance(employee, first_day, 'Present')
|
||||
month_name = get_month_name(first_day)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
# holiday considered in unmarked days
|
||||
self.assertIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_excluding_holidays(self):
|
||||
first_day = get_first_day(getdate())
|
||||
|
||||
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
|
||||
frappe.db.delete('Attendance', {'employee': employee})
|
||||
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
holiday_list = make_holiday_list()
|
||||
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(holiday_list)
|
||||
mark_attendance(employee, first_day, 'Present')
|
||||
month_name = get_month_name(first_day)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
# holidays not considered in unmarked days
|
||||
self.assertNotIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
||||
first_day = get_first_day(getdate())
|
||||
|
||||
doj = add_days(first_day, 1)
|
||||
relieving_date = add_days(first_day, 5)
|
||||
employee = make_employee('test_unmarked_days_as_per_doj@example.com', date_of_joining=doj,
|
||||
date_of_relieving=relieving_date)
|
||||
frappe.db.delete('Attendance', {'employee': employee})
|
||||
|
||||
attendance_date = add_days(first_day, 2)
|
||||
mark_attendance(employee, attendance_date, 'Present')
|
||||
month_name = get_month_name(first_day)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(attendance_date, unmarked_days)
|
||||
# date before doj not in unmarked days
|
||||
self.assertNotIn(add_days(doj, -1), unmarked_days)
|
||||
# date after relieving not in unmarked days
|
||||
self.assertNotIn(add_days(relieving_date, 1), unmarked_days)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def get_month_name(date):
|
||||
month_number = date.month
|
||||
for month, number in get_month_map().items():
|
||||
if number == month_number:
|
||||
return month
|
||||
@@ -8,7 +8,7 @@ def get_data():
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Manufacture'),
|
||||
'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet']
|
||||
'items': ['BOM', 'Work Order', 'Job Card']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ frappe.ui.form.on('Production Plan', {
|
||||
if (d.item_code) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.bom",
|
||||
filters:{'item': cstr(d.item_code)}
|
||||
filters:{'item': cstr(d.item_code), 'docstatus': 1}
|
||||
}
|
||||
} else frappe.msgprint(__("Please enter Item first"));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_sales_orders,
|
||||
get_warehouse_list,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
@@ -466,26 +467,29 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
bom = make_bom(item=item, raw_materials=raw_materials)
|
||||
|
||||
# Create Production Plan
|
||||
pln = create_production_plan(item_code=bom.item, planned_qty=10)
|
||||
pln = create_production_plan(item_code=bom.item, planned_qty=5)
|
||||
|
||||
# All the created Work Orders
|
||||
wo_list = []
|
||||
|
||||
# Create and Submit 1st Work Order for 5 qty
|
||||
create_work_order(item, pln, 5)
|
||||
# Create and Submit 1st Work Order for 3 qty
|
||||
create_work_order(item, pln, 3)
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].ordered_qty, 3)
|
||||
|
||||
# Create and Submit 2nd Work Order for 2 qty
|
||||
create_work_order(item, pln, 2)
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].ordered_qty, 5)
|
||||
|
||||
# Create and Submit 2nd Work Order for 3 qty
|
||||
create_work_order(item, pln, 3)
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].ordered_qty, 8)
|
||||
# Overproduction
|
||||
self.assertRaises(OverProductionError, create_work_order, item=item, pln=pln, qty=2)
|
||||
|
||||
# Cancel 1st Work Order
|
||||
wo1 = frappe.get_doc("Work Order", wo_list[0])
|
||||
wo1.cancel()
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].ordered_qty, 3)
|
||||
self.assertEqual(pln.po_items[0].ordered_qty, 2)
|
||||
|
||||
# Cancel 2nd Work Order
|
||||
wo2 = frappe.get_doc("Work Order", wo_list[1])
|
||||
|
||||
@@ -17,7 +17,7 @@ frappe.ui.form.on('Routing', {
|
||||
},
|
||||
|
||||
calculate_operating_cost: function(frm, child) {
|
||||
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2);
|
||||
const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child));
|
||||
frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +20,8 @@ class Routing(Document):
|
||||
for operation in self.operations:
|
||||
if not operation.hour_rate:
|
||||
operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate')
|
||||
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2)
|
||||
operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60,
|
||||
operation.precision("operating_cost"))
|
||||
|
||||
def set_routing_id(self):
|
||||
sequence_id = 0
|
||||
|
||||
@@ -632,6 +632,21 @@ class WorkOrder(Document):
|
||||
if not self.qty > 0:
|
||||
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
|
||||
|
||||
if self.production_plan and self.production_plan_item:
|
||||
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
|
||||
|
||||
allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
|
||||
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
|
||||
|
||||
max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
|
||||
|
||||
if max_qty < 1:
|
||||
frappe.throw(_("Cannot produce more item for {0}")
|
||||
.format(self.production_item), OverProductionError)
|
||||
elif self.qty > max_qty:
|
||||
frappe.throw(_("Cannot produce more than {0} items for {1}")
|
||||
.format(max_qty, self.production_item), OverProductionError)
|
||||
|
||||
def validate_transfer_against(self):
|
||||
if not self.docstatus == 1:
|
||||
# let user configure operations until they're ready to submit
|
||||
@@ -835,7 +850,7 @@ def get_item_details(item, project = None, skip_bom_info=False):
|
||||
res = res[0]
|
||||
if skip_bom_info: return res
|
||||
|
||||
filters = {"item": item, "is_default": 1}
|
||||
filters = {"item": item, "is_default": 1, "docstatus": 1}
|
||||
|
||||
if project:
|
||||
filters = {"item": item, "project": project}
|
||||
|
||||
@@ -12,9 +12,9 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('Transaction'),
|
||||
'items': ['Work Order', 'Job Card', 'Timesheet']
|
||||
'items': ['Work Order', 'Job Card',]
|
||||
}
|
||||
],
|
||||
'disable_create_buttons': ['BOM', 'Routing', 'Operation',
|
||||
'Work Order', 'Job Card', 'Timesheet']
|
||||
'Work Order', 'Job Card',]
|
||||
}
|
||||
|
||||
@@ -308,28 +308,59 @@ class SalarySlip(TransactionBase):
|
||||
if payroll_based_on == "Attendance":
|
||||
self.payment_days -= flt(absent)
|
||||
|
||||
unmarked_days = self.get_unmarked_days()
|
||||
consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
|
||||
|
||||
if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
|
||||
unmarked_days = self.get_unmarked_days(include_holidays_in_total_working_days)
|
||||
self.absent_days += unmarked_days #will be treated as absent
|
||||
self.payment_days -= unmarked_days
|
||||
if include_holidays_in_total_working_days:
|
||||
for holiday in holidays:
|
||||
if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
|
||||
self.payment_days += 1
|
||||
else:
|
||||
self.payment_days = 0
|
||||
|
||||
def get_unmarked_days(self):
|
||||
marked_days = frappe.get_all("Attendance", filters = {
|
||||
"attendance_date": ["between", [self.start_date, self.end_date]],
|
||||
"employee": self.employee,
|
||||
"docstatus": 1
|
||||
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
|
||||
def get_unmarked_days(self, include_holidays_in_total_working_days):
|
||||
unmarked_days = self.total_working_days
|
||||
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
|
||||
["date_of_joining", "relieving_date"])
|
||||
start_date = self.start_date
|
||||
end_date = self.end_date
|
||||
|
||||
return self.total_working_days - marked_days
|
||||
if joining_date and (getdate(self.start_date) < joining_date <= getdate(self.end_date)):
|
||||
start_date = joining_date
|
||||
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
|
||||
include_holidays_in_total_working_days, self.start_date, joining_date)
|
||||
|
||||
if relieving_date and (getdate(self.start_date) <= relieving_date < getdate(self.end_date)):
|
||||
end_date = relieving_date
|
||||
unmarked_days = self.get_unmarked_days_based_on_doj_or_relieving(unmarked_days,
|
||||
include_holidays_in_total_working_days, relieving_date, self.end_date)
|
||||
|
||||
# exclude days for which attendance has been marked
|
||||
unmarked_days -= frappe.get_all("Attendance", filters = {
|
||||
"attendance_date": ["between", [start_date, end_date]],
|
||||
"employee": self.employee,
|
||||
"docstatus": 1
|
||||
}, fields = ["COUNT(*) as marked_days"])[0].marked_days
|
||||
|
||||
return unmarked_days
|
||||
|
||||
def get_unmarked_days_based_on_doj_or_relieving(self, unmarked_days,
|
||||
include_holidays_in_total_working_days, start_date, end_date):
|
||||
"""
|
||||
Exclude days before DOJ or after
|
||||
Relieving Date from unmarked days
|
||||
"""
|
||||
from erpnext.hr.doctype.employee.employee import is_holiday
|
||||
|
||||
if include_holidays_in_total_working_days:
|
||||
unmarked_days -= date_diff(end_date, start_date)
|
||||
else:
|
||||
# exclude only if not holidays
|
||||
for days in range(date_diff(end_date, start_date)):
|
||||
date = add_days(end_date, -days)
|
||||
if not is_holiday(self.employee, date):
|
||||
unmarked_days -= 1
|
||||
|
||||
return unmarked_days
|
||||
|
||||
def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
|
||||
if not joining_date:
|
||||
|
||||
@@ -7,10 +7,12 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
@@ -21,6 +23,7 @@ from frappe.utils.make_random import get_random
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
@@ -37,17 +40,17 @@ class TestSalarySlip(unittest.TestCase):
|
||||
setup_test()
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"payroll_based_on": "Attendance",
|
||||
"daily_wages_fraction_for_half_day": 0.75
|
||||
})
|
||||
def test_payment_days_based_on_attendance(self):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
no_of_days = self.get_no_of_days()
|
||||
|
||||
# Payroll based on attendance
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
|
||||
frappe.db.set_value("Payroll Settings", None, "daily_wages_fraction_for_half_day", 0.75)
|
||||
|
||||
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
|
||||
@@ -85,14 +88,78 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
self.assertEqual(ss.gross_pay, gross_pay)
|
||||
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
||||
@change_settings("Payroll Settings", {
|
||||
"payroll_based_on": "Attendance",
|
||||
"consider_unmarked_attendance_as": "Absent",
|
||||
"include_holidays_in_total_working_days": True
|
||||
})
|
||||
def test_payment_days_for_mid_joinee_including_holidays(self):
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
|
||||
frappe.db.set_value("Employee", new_emp_id, {
|
||||
"date_of_joining": joining_date,
|
||||
"relieving_date": relieving_date,
|
||||
"status": "Left"
|
||||
})
|
||||
|
||||
holidays = 0
|
||||
|
||||
for days in range(date_diff(relieving_date, joining_date) + 1):
|
||||
date = add_days(joining_date, days)
|
||||
if not is_holiday("Salary Slip Test Holiday List", date):
|
||||
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
|
||||
else:
|
||||
holidays += 1
|
||||
|
||||
new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
|
||||
|
||||
self.assertEqual(new_ss.total_working_days, no_of_days[0])
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"payroll_based_on": "Attendance",
|
||||
"consider_unmarked_attendance_as": "Absent",
|
||||
"include_holidays_in_total_working_days": False
|
||||
})
|
||||
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
joining_date, relieving_date = add_days(month_start_date, 3), add_days(month_end_date, -5)
|
||||
frappe.db.set_value("Employee", new_emp_id, {
|
||||
"date_of_joining": joining_date,
|
||||
"relieving_date": relieving_date,
|
||||
"status": "Left"
|
||||
})
|
||||
|
||||
holidays = 0
|
||||
|
||||
for days in range(date_diff(relieving_date, joining_date) + 1):
|
||||
date = add_days(joining_date, days)
|
||||
if not is_holiday("Salary Slip Test Holiday List", date):
|
||||
mark_attendance(new_emp_id, date, 'Present', ignore_validate=True)
|
||||
else:
|
||||
holidays += 1
|
||||
|
||||
new_ss = make_employee_salary_slip("test_payment_days_based_on_joining_date@salary.com", "Monthly", "Test Payment Based On Attendence")
|
||||
|
||||
self.assertEqual(new_ss.total_working_days, no_of_days[0] - no_of_days[1])
|
||||
self.assertEqual(new_ss.payment_days, no_of_days[0] - holidays - 8)
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"payroll_based_on": "Leave"
|
||||
})
|
||||
def test_payment_days_based_on_leave_application(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
|
||||
# Payroll based on attendance
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
|
||||
@@ -133,8 +200,9 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 4)
|
||||
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"payroll_based_on": "Attendance"
|
||||
})
|
||||
def test_payment_days_in_salary_slip_based_on_timesheet(self):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
from erpnext.projects.doctype.timesheet.test_timesheet import (
|
||||
@@ -145,9 +213,6 @@ class TestSalarySlip(unittest.TestCase):
|
||||
make_salary_slip as make_salary_slip_for_timesheet,
|
||||
)
|
||||
|
||||
# Payroll based on attendance
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
|
||||
|
||||
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company", holiday_list="Salary Slip Test Holiday List")
|
||||
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
|
||||
|
||||
@@ -185,8 +250,9 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
|
||||
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"payroll_based_on": "Attendance"
|
||||
})
|
||||
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
@@ -194,9 +260,6 @@ class TestSalarySlip(unittest.TestCase):
|
||||
)
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
# Payroll based on attendance
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
|
||||
|
||||
salary_structure = make_salary_structure_for_payment_days_based_component_dependency()
|
||||
employee = make_employee("test_payment_days_based_component@salary.com", company="_Test Company")
|
||||
|
||||
@@ -242,11 +305,12 @@ class TestSalarySlip(unittest.TestCase):
|
||||
expected_amount = flt((flt(ss.gross_pay) - payment_days_based_comp_amount) * 0.12, precision)
|
||||
|
||||
self.assertEqual(actual_amount, expected_amount)
|
||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"include_holidays_in_total_working_days": 1
|
||||
})
|
||||
def test_salary_slip_with_holidays_included(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
|
||||
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
||||
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
||||
{"employee_name":"test_salary_slip_with_holidays_included@salary.com"}, "name"), "relieving_date", None)
|
||||
@@ -260,9 +324,11 @@ class TestSalarySlip(unittest.TestCase):
|
||||
self.assertEqual(ss.earnings[1].amount, 3000)
|
||||
self.assertEqual(ss.gross_pay, 78000)
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"include_holidays_in_total_working_days": 0
|
||||
})
|
||||
def test_salary_slip_with_holidays_excluded(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
|
||||
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
||||
frappe.db.set_value("Employee", frappe.get_value("Employee",
|
||||
{"employee_name":"test_salary_slip_with_holidays_excluded@salary.com"}, "name"), "relieving_date", None)
|
||||
@@ -277,14 +343,15 @@ class TestSalarySlip(unittest.TestCase):
|
||||
self.assertEqual(ss.earnings[1].amount, 3000)
|
||||
self.assertEqual(ss.gross_pay, 78000)
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"include_holidays_in_total_working_days": 1
|
||||
})
|
||||
def test_payment_days(self):
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
create_salary_structure_assignment,
|
||||
)
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
# Holidays not included in working days
|
||||
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 1)
|
||||
|
||||
# set joinng date in the same month
|
||||
employee = make_employee("test_payment_days@salary.com")
|
||||
@@ -342,11 +409,12 @@ class TestSalarySlip(unittest.TestCase):
|
||||
frappe.set_user("test_employee_salary_slip_read_permission@salary.com")
|
||||
self.assertTrue(salary_slip_test_employee.has_permission("read"))
|
||||
|
||||
@change_settings("Payroll Settings", {
|
||||
"email_salary_slip_to_employee": 1
|
||||
})
|
||||
def test_email_salary_slip(self):
|
||||
frappe.db.sql("delete from `tabEmail Queue`")
|
||||
|
||||
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 1)
|
||||
|
||||
make_employee("test_email_salary_slip@salary.com")
|
||||
ss = make_employee_salary_slip("test_email_salary_slip@salary.com", "Monthly", "Test Salary Slip Email")
|
||||
ss.company = "_Test Company"
|
||||
|
||||
@@ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", {
|
||||
|
||||
currency: function(frm) {
|
||||
let base_currency = frappe.defaults.get_global_default('currency');
|
||||
if (base_currency != frm.doc.currency) {
|
||||
if (frm.doc.currency && (base_currency != frm.doc.currency)) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
|
||||
@@ -2265,13 +2265,17 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
},
|
||||
|
||||
coupon_code: function() {
|
||||
var me = this;
|
||||
frappe.run_serially([
|
||||
() => this.frm.doc.ignore_pricing_rule=1,
|
||||
() => me.ignore_pricing_rule(),
|
||||
() => this.frm.doc.ignore_pricing_rule=0,
|
||||
() => me.apply_pricing_rule()
|
||||
]);
|
||||
if (this.frm.doc.coupon_code || this.frm._last_coupon_code) {
|
||||
// reset pricing rules if coupon code is set or is unset
|
||||
const _ignore_pricing_rule = this.frm.doc.ignore_pricing_rule;
|
||||
return frappe.run_serially([
|
||||
() => this.frm.doc.ignore_pricing_rule=1,
|
||||
() => this.frm.trigger('ignore_pricing_rule'),
|
||||
() => this.frm.doc.ignore_pricing_rule=_ignore_pricing_rule,
|
||||
() => this.frm.trigger('apply_pricing_rule'),
|
||||
() => this.frm._last_coupon_code = this.frm.doc.coupon_code
|
||||
]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -304,12 +304,13 @@ erpnext.HierarchyChart = class {
|
||||
}
|
||||
|
||||
get_child_nodes(node_id) {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: this.method,
|
||||
method: me.method,
|
||||
args: {
|
||||
parent: node_id,
|
||||
company: this.company
|
||||
company: me.company
|
||||
}
|
||||
}).then(r => resolve(r.message));
|
||||
});
|
||||
@@ -350,12 +351,13 @@ erpnext.HierarchyChart = class {
|
||||
}
|
||||
|
||||
get_all_nodes() {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'erpnext.utilities.hierarchy_chart.get_all_nodes',
|
||||
args: {
|
||||
method: this.method,
|
||||
company: this.company
|
||||
method: me.method,
|
||||
company: me.company
|
||||
},
|
||||
callback: (r) => {
|
||||
resolve(r.message);
|
||||
@@ -427,8 +429,8 @@ erpnext.HierarchyChart = class {
|
||||
|
||||
add_connector(parent_id, child_id) {
|
||||
// using pure javascript for better performance
|
||||
const parent_node = document.querySelector(`#${parent_id}`);
|
||||
const child_node = document.querySelector(`#${child_id}`);
|
||||
const parent_node = document.getElementById(`${parent_id}`);
|
||||
const child_node = document.getElementById(`${child_id}`);
|
||||
|
||||
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ erpnext.HierarchyChartMobile = class {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: this.method,
|
||||
method: me.method,
|
||||
args: {
|
||||
parent: node_id,
|
||||
company: me.company,
|
||||
@@ -286,8 +286,8 @@ erpnext.HierarchyChartMobile = class {
|
||||
}
|
||||
|
||||
add_connector(parent_id, child_id) {
|
||||
const parent_node = document.querySelector(`#${parent_id}`);
|
||||
const child_node = document.querySelector(`#${child_id}`);
|
||||
const parent_node = document.getElementById(`${parent_id}`);
|
||||
const child_node = document.getElementById(`${child_id}`);
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
|
||||
@@ -518,7 +518,8 @@ erpnext.HierarchyChartMobile = class {
|
||||
level.nextAll('li').remove();
|
||||
|
||||
let node_object = this.nodes[node.id];
|
||||
let current_node = level.find(`#${node.id}`).detach();
|
||||
let current_node = level.find(`[id="${node.id}"]`).detach();
|
||||
|
||||
current_node.removeClass('active-child active-path');
|
||||
|
||||
node_object.expanded = 0;
|
||||
|
||||
@@ -23,9 +23,5 @@
|
||||
"StateCesAmt": "{item.state_cess_amount}",
|
||||
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
|
||||
"OthChrg": "{item.other_charges}",
|
||||
"TotItemVal": "{item.total_value}",
|
||||
"BchDtls": {{
|
||||
"Nm": "{item.batch_no}",
|
||||
"ExpDt": "{item.batch_expiry_date}"
|
||||
}}
|
||||
"TotItemVal": "{item.total_value}"
|
||||
}}
|
||||
@@ -215,8 +215,6 @@ def get_item_list(invoice):
|
||||
item.taxable_value = abs(item.taxable_value)
|
||||
item.discount_amount = 0
|
||||
|
||||
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
|
||||
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
|
||||
item.is_service_item = 'Y' if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else 'N'
|
||||
item.serial_no = ""
|
||||
|
||||
|
||||
@@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
var me = this;
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Items"),
|
||||
size: "large",
|
||||
fields: [
|
||||
{
|
||||
"fieldtype": "Check",
|
||||
@@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
} else {
|
||||
let po_items = [];
|
||||
me.frm.doc.items.forEach(d => {
|
||||
let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
|
||||
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
|
||||
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
|
||||
if (pending_qty > 0) {
|
||||
po_items.push({
|
||||
"doctype": "Sales Order Item",
|
||||
@@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
get_ordered_qty: function(item, so) {
|
||||
let ordered_qty = item.ordered_qty;
|
||||
if (so.packed_items) {
|
||||
// calculate ordered qty based on packed items in case of product bundle
|
||||
let packed_items = so.packed_items.filter(
|
||||
(pi) => pi.parent_detail_docname == item.name
|
||||
);
|
||||
if (packed_items) {
|
||||
ordered_qty = packed_items.reduce(
|
||||
(sum, pi) => sum + flt(pi.ordered_qty),
|
||||
0
|
||||
);
|
||||
ordered_qty = ordered_qty / packed_items.length;
|
||||
}
|
||||
}
|
||||
return ordered_qty;
|
||||
},
|
||||
|
||||
hold_sales_order: function(){
|
||||
var me = this;
|
||||
var d = new frappe.ui.Dialog({
|
||||
|
||||
@@ -923,6 +923,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
||||
target.project = source_parent.project
|
||||
|
||||
def update_item_for_packed_item(source, target, source_parent):
|
||||
target.qty = flt(source.qty) - flt(source.ordered_qty)
|
||||
|
||||
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
||||
doc = get_mapped_doc("Sales Order", source_name, {
|
||||
"Sales Order": {
|
||||
@@ -966,6 +969,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"Packed Item": {
|
||||
"doctype": "Purchase Order Item",
|
||||
"field_map": [
|
||||
["name", "sales_order_packed_item"],
|
||||
["parent", "sales_order"],
|
||||
["uom", "uom"],
|
||||
["conversion_factor", "conversion_factor"],
|
||||
@@ -980,6 +984,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"supplier",
|
||||
"pricing_rules"
|
||||
],
|
||||
"postprocess": update_item_for_packed_item,
|
||||
"condition": lambda doc: doc.parent_item in items_to_map
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
@@ -921,6 +921,74 @@ class TestSalesOrder(ERPNextTestCase):
|
||||
self.assertEqual(purchase_orders[0].supplier, '_Test Supplier')
|
||||
self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1')
|
||||
|
||||
def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self):
|
||||
"""
|
||||
Tests if the the Product Bundles in the Items table of Sales Orders are replaced with
|
||||
their child items(from the Packed Items table) on creating a Purchase Order from it.
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
|
||||
make_product_bundle("_Test Product Bundle",
|
||||
["_Test Bundle Item 1", "_Test Bundle Item 2"])
|
||||
|
||||
so_items = [
|
||||
{
|
||||
"item_code": product_bundle.item_code,
|
||||
"warehouse": "",
|
||||
"qty": 2,
|
||||
"rate": 400,
|
||||
"delivered_by_supplier": 1,
|
||||
"supplier": '_Test Supplier'
|
||||
}
|
||||
]
|
||||
|
||||
so = make_sales_order(item_list=so_items)
|
||||
|
||||
purchase_order = make_purchase_order(so.name, selected_items=so_items)
|
||||
|
||||
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
|
||||
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
|
||||
|
||||
def test_purchase_order_updates_packed_item_ordered_qty(self):
|
||||
"""
|
||||
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
|
||||
make_product_bundle("_Test Product Bundle",
|
||||
["_Test Bundle Item 1", "_Test Bundle Item 2"])
|
||||
|
||||
so_items = [
|
||||
{
|
||||
"item_code": product_bundle.item_code,
|
||||
"warehouse": "",
|
||||
"qty": 2,
|
||||
"rate": 400,
|
||||
"delivered_by_supplier": 1,
|
||||
"supplier": '_Test Supplier'
|
||||
}
|
||||
]
|
||||
|
||||
so = make_sales_order(item_list=so_items)
|
||||
|
||||
purchase_order = make_purchase_order(so.name, selected_items=so_items)
|
||||
purchase_order.supplier = "_Test Supplier"
|
||||
purchase_order.set_warehouse = "_Test Warehouse - _TC"
|
||||
purchase_order.save()
|
||||
purchase_order.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.packed_items[0].ordered_qty, 2)
|
||||
self.assertEqual(so.packed_items[1].ordered_qty, 2)
|
||||
|
||||
def test_reserved_qty_for_closing_so(self):
|
||||
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
||||
fields=["reserved_qty"])
|
||||
|
||||
@@ -83,8 +83,8 @@
|
||||
"planned_qty",
|
||||
"column_break_69",
|
||||
"work_order_qty",
|
||||
"produced_qty",
|
||||
"delivered_qty",
|
||||
"produced_qty",
|
||||
"returned_qty",
|
||||
"shopping_cart_section",
|
||||
"additional_notes",
|
||||
@@ -701,8 +701,10 @@
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
"description": "For Production",
|
||||
"fieldname": "produced_qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Produced Quantity",
|
||||
"oldfieldname": "produced_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
@@ -791,6 +793,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
@@ -800,7 +803,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-21 13:55:08.883104",
|
||||
"modified": "2022-02-24 14:41:57.325799",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
||||
@@ -170,17 +170,20 @@ erpnext.PointOfSale.Payment = class {
|
||||
});
|
||||
|
||||
frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => {
|
||||
if (!frm.doc.ignore_pricing_rule) {
|
||||
if (frm.doc.coupon_code) {
|
||||
frappe.run_serially([
|
||||
() => frm.doc.ignore_pricing_rule=1,
|
||||
() => frm.trigger('ignore_pricing_rule'),
|
||||
() => frm.doc.ignore_pricing_rule=0,
|
||||
() => frm.trigger('apply_pricing_rule'),
|
||||
() => frm.save(),
|
||||
() => this.update_totals_section(frm.doc)
|
||||
]);
|
||||
}
|
||||
if (!frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
|
||||
frappe.run_serially([
|
||||
() => frm.doc.ignore_pricing_rule=1,
|
||||
() => frm.trigger('ignore_pricing_rule'),
|
||||
() => frm.doc.ignore_pricing_rule=0,
|
||||
() => frm.trigger('apply_pricing_rule'),
|
||||
() => frm.save(),
|
||||
() => this.update_totals_section(frm.doc)
|
||||
]);
|
||||
} else if (frm.doc.ignore_pricing_rule && frm.doc.coupon_code) {
|
||||
frappe.show_alert({
|
||||
message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."),
|
||||
indicator: "orange"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -757,6 +757,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
@@ -767,12 +768,14 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-06 12:12:44.018872",
|
||||
"modified": "2022-02-24 14:42:20.211085",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
@@ -399,6 +399,7 @@ class Item(Document):
|
||||
|
||||
if merge:
|
||||
self.validate_properties_before_merge(new_name)
|
||||
self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
|
||||
self.validate_duplicate_website_item_before_merge(old_name, new_name)
|
||||
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
@@ -463,6 +464,20 @@ class Item(Document):
|
||||
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
|
||||
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
|
||||
"Block merge if both old and new items have product bundles."
|
||||
old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
|
||||
new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
|
||||
|
||||
if old_bundle and new_bundle:
|
||||
bundle_link = get_link_to_form("Product Bundle", old_bundle)
|
||||
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
|
||||
|
||||
msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
|
||||
bundle_link, old_name, new_name
|
||||
)
|
||||
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def validate_duplicate_website_item_before_merge(self, old_name, new_name):
|
||||
"""
|
||||
Block merge if both old and new items have website items against them.
|
||||
@@ -480,8 +495,9 @@ class Item(Document):
|
||||
|
||||
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
|
||||
web_item_link = get_link_to_form("Website Item", old_web_item)
|
||||
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
|
||||
|
||||
msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}"
|
||||
msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
|
||||
frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def set_last_purchase_rate(self, new_name):
|
||||
|
||||
@@ -14,6 +14,7 @@ from erpnext.controllers.item_variant import (
|
||||
get_variant,
|
||||
)
|
||||
from erpnext.stock.doctype.item.item import (
|
||||
DataValidationError,
|
||||
InvalidBarcode,
|
||||
StockExistsForTemplate,
|
||||
get_item_attribute,
|
||||
@@ -387,6 +388,26 @@ class TestItem(ERPNextTestCase):
|
||||
self.assertTrue(frappe.db.get_value("Bin",
|
||||
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
|
||||
|
||||
def test_item_merging_with_product_bundle(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
|
||||
create_item("Test Item Bundle Item 1", is_stock_item=False)
|
||||
create_item("Test Item Bundle Item 2", is_stock_item=False)
|
||||
create_item("Test Item inside Bundle")
|
||||
bundle_items = ["Test Item inside Bundle"]
|
||||
|
||||
# make bundles for both items
|
||||
bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
|
||||
make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
|
||||
|
||||
with self.assertRaises(DataValidationError):
|
||||
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
|
||||
|
||||
bundle1.delete()
|
||||
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
|
||||
|
||||
self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
|
||||
|
||||
def test_uom_conversion_factor(self):
|
||||
if frappe.db.exists('Item', 'Test Item UOM'):
|
||||
frappe.delete_doc('Item', 'Test Item UOM')
|
||||
|
||||
@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.utils import update_gl_entries_after
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
get_gl_entries,
|
||||
make_purchase_receipt,
|
||||
@@ -177,6 +178,53 @@ class TestLandedCostVoucher(ERPNextTestCase):
|
||||
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
|
||||
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
|
||||
|
||||
def test_serialized_lcv_delivered(self):
|
||||
"""In some cases you'd want to deliver before you can know all the
|
||||
landed costs, this should be allowed for serial nos too.
|
||||
|
||||
Case:
|
||||
- receipt a serial no @ X rate
|
||||
- delivery the serial no @ X rate
|
||||
- add LCV to receipt X + Y
|
||||
- LCV should be successful
|
||||
- delivery should reflect X+Y valuation.
|
||||
"""
|
||||
serial_no = "LCV_TEST_SR_NO"
|
||||
item_code = "_Test Serialized Item"
|
||||
warehouse = "Stores - TCP1"
|
||||
|
||||
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
|
||||
warehouse=warehouse, qty=1, rate=200,
|
||||
item_code=item_code, serial_no=serial_no)
|
||||
|
||||
serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
|
||||
|
||||
# deliver it before creating LCV
|
||||
dn = create_delivery_note(item_code=item_code,
|
||||
company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
|
||||
serial_no=serial_no, qty=1, rate=500,
|
||||
cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
|
||||
|
||||
charges = 10
|
||||
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
|
||||
|
||||
new_purchase_rate = serial_no_rate + charges
|
||||
|
||||
serial_no = frappe.db.get_value("Serial No", serial_no,
|
||||
["warehouse", "purchase_rate"], as_dict=1)
|
||||
|
||||
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
|
||||
|
||||
stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
|
||||
filters={
|
||||
"voucher_no": dn.name,
|
||||
"voucher_type": dn.doctype,
|
||||
"is_cancelled": 0 # LCV cancels with same name.
|
||||
},
|
||||
fieldname="stock_value_difference")
|
||||
|
||||
# reposting should update the purchase rate in future delivery
|
||||
self.assertEqual(stock_value_difference, -new_purchase_rate)
|
||||
|
||||
def test_landed_cost_voucher_for_odd_numbers (self):
|
||||
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
|
||||
|
||||
@@ -626,13 +626,13 @@ class TestMaterialRequest(ERPNextTestCase):
|
||||
mr.schedule_date = today()
|
||||
|
||||
if not frappe.db.get_value('UOM Conversion Detail',
|
||||
{'parent': item.item_code, 'uom': 'Kg'}):
|
||||
item_doc = frappe.get_doc('Item', item.item_code)
|
||||
item_doc.append('uoms', {
|
||||
'uom': 'Kg',
|
||||
'conversion_factor': 5
|
||||
})
|
||||
item_doc.save(ignore_permissions=True)
|
||||
{'parent': item.item_code, 'uom': 'Kg'}):
|
||||
item_doc = frappe.get_doc('Item', item.item_code)
|
||||
item_doc.append('uoms', {
|
||||
'uom': 'Kg',
|
||||
'conversion_factor': 5
|
||||
})
|
||||
item_doc.save(ignore_permissions=True)
|
||||
|
||||
item.uom = 'Kg'
|
||||
for item in mr.items:
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"section_break_13",
|
||||
"actual_qty",
|
||||
"projected_qty",
|
||||
"ordered_qty",
|
||||
"column_break_16",
|
||||
"incoming_rate",
|
||||
"page_break",
|
||||
@@ -224,13 +225,21 @@
|
||||
"label": "Rate",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "ordered_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Ordered Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-28 16:03:30.780111",
|
||||
"modified": "2022-02-22 12:57:45.325488",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Packed Item",
|
||||
|
||||
@@ -162,6 +162,15 @@ class TestPurchaseReceipt(ERPNextTestCase):
|
||||
qty=abs(existing_bin_qty)
|
||||
)
|
||||
|
||||
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
|
||||
"Bin",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC"
|
||||
},
|
||||
["actual_qty", "stock_value"]
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
|
||||
@@ -627,6 +627,12 @@ frappe.ui.form.on('Stock Entry Detail', {
|
||||
frm.events.set_serial_no(frm, cdt, cdn, () => {
|
||||
frm.events.get_warehouse_details(frm, cdt, cdn);
|
||||
});
|
||||
|
||||
// set allow_zero_valuation_rate to 0 if s_warehouse is selected.
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (item.s_warehouse) {
|
||||
item.allow_zero_valuation_rate = 0;
|
||||
}
|
||||
},
|
||||
|
||||
t_warehouse: function(frm, cdt, cdn) {
|
||||
|
||||
@@ -45,6 +45,7 @@ def get_sle(**args):
|
||||
|
||||
class TestStockEntry(ERPNextTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
|
||||
|
||||
@@ -566,6 +567,7 @@ class TestStockEntry(ERPNextTestCase):
|
||||
st1.set_stock_entry_type()
|
||||
st1.insert()
|
||||
st1.submit()
|
||||
st1.cancel()
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
|
||||
@@ -690,6 +692,8 @@ class TestStockEntry(ERPNextTestCase):
|
||||
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
|
||||
"is_default": 1, "docstatus": 1})
|
||||
|
||||
make_item_variant() # make variant of _Test Variant Item if absent
|
||||
|
||||
work_order = frappe.new_doc("Work Order")
|
||||
work_order.update({
|
||||
"company": "_Test Company",
|
||||
@@ -1101,13 +1105,10 @@ class TestStockEntry(ERPNextTestCase):
|
||||
|
||||
# Check if FG cost is calculated based on RM total cost
|
||||
# RM total cost = 200, FG rate = 200/4(FG qty) = 50
|
||||
self.assertEqual(se.items[1].basic_rate, 50)
|
||||
self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4))
|
||||
self.assertEqual(se.value_difference, 0.0)
|
||||
self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
|
||||
|
||||
# teardown
|
||||
se.delete()
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
se = frappe.copy_doc(test_records[0])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-03-29 18:22:12",
|
||||
"creation": "2022-02-05 00:17:49.860824",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"editable_grid": 1,
|
||||
@@ -340,13 +340,13 @@
|
||||
"label": "More Information"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only_depends_on": "eval:doc.s_warehouse"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -556,12 +556,14 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-22 16:47:11.268975",
|
||||
"modified": "2022-02-26 00:51:24.963653",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.utils import add_days, today
|
||||
@@ -32,6 +34,27 @@ class TestStockLedgerEntry(ERPNextTestCase):
|
||||
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 assertSLEs(self, doc, expected_sles, sle_filters=None):
|
||||
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
|
||||
|
||||
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
|
||||
if sle_filters:
|
||||
filters.update(sle_filters)
|
||||
sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation")
|
||||
|
||||
for exp_sle, act_sle in zip(expected_sles, sles):
|
||||
for k, v in exp_sle.items():
|
||||
act_value = act_sle[k]
|
||||
if k == "stock_queue":
|
||||
act_value = json.loads(act_value)
|
||||
if act_value and act_value[0][0] == 0:
|
||||
# ignore empty fifo bins
|
||||
continue
|
||||
|
||||
self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
|
||||
|
||||
def test_item_cost_reposting(self):
|
||||
company = "_Test Company"
|
||||
|
||||
@@ -349,6 +372,77 @@ class TestStockLedgerEntry(ERPNextTestCase):
|
||||
frappe.set_user("Administrator")
|
||||
user.remove_roles("Stock Manager")
|
||||
|
||||
def test_fifo_dependent_consumption(self):
|
||||
item = make_item("_TestFifoTransferRates")
|
||||
source = "_Test Warehouse - _TC"
|
||||
target = "Stores - _TC"
|
||||
|
||||
rates = [10 * i for i in range(1, 20)]
|
||||
|
||||
receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||
row.basic_rate = rate
|
||||
receipt.append("items", row)
|
||||
|
||||
receipt.save()
|
||||
receipt.submit()
|
||||
|
||||
expected_queues = []
|
||||
for idx, rate in enumerate(rates, start=1):
|
||||
expected_queues.append(
|
||||
{"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
|
||||
)
|
||||
self.assertSLEs(receipt, expected_queues)
|
||||
|
||||
transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
|
||||
transfer.append("items", row)
|
||||
|
||||
transfer.save()
|
||||
transfer.submit()
|
||||
|
||||
# same exact queue should be transferred
|
||||
self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
|
||||
|
||||
def test_fifo_multi_item_repack_consumption(self):
|
||||
rm = make_item("_TestFifoRepackRM")
|
||||
packed = make_item("_TestFifoRepackFinished")
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
rates = [10 * i for i in range(1, 5)]
|
||||
|
||||
receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||
row.basic_rate = rate
|
||||
receipt.append("items", row)
|
||||
|
||||
receipt.save()
|
||||
receipt.submit()
|
||||
|
||||
repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
|
||||
do_not_save=True, rate=10, purpose="Repack")
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
|
||||
repack.append("items", row)
|
||||
|
||||
repack.append("items", {
|
||||
"item_code": packed.name,
|
||||
"t_warehouse": warehouse,
|
||||
"qty": 1,
|
||||
"transfer_qty": 1,
|
||||
})
|
||||
|
||||
repack.save()
|
||||
repack.submit()
|
||||
|
||||
# same exact queue should be transferred
|
||||
self.assertSLEs(repack, [
|
||||
{"incoming_rate": sum(rates) * 10}
|
||||
], sle_filters={"item_code": packed.name})
|
||||
|
||||
|
||||
def create_repack_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -244,7 +244,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-03 04:40:06.414630",
|
||||
"modified": "2022-03-01 02:37:48.034944",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
@@ -301,5 +301,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "warehouse_name"
|
||||
"states": [],
|
||||
"title_field": "warehouse_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
@@ -21,6 +21,7 @@ SLE_FIELDS = (
|
||||
"stock_value",
|
||||
"stock_value_difference",
|
||||
"valuation_rate",
|
||||
"voucher_detail_no",
|
||||
)
|
||||
|
||||
|
||||
@@ -60,10 +61,15 @@ def add_invariant_check_fields(sles):
|
||||
fifo_qty += qty
|
||||
fifo_value += qty * rate
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
|
||||
|
||||
balance_qty += sle.actual_qty
|
||||
balance_stock_value += sle.stock_value_difference
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||
balance_qty = sle.qty_after_transaction
|
||||
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
|
||||
if balance_qty is None:
|
||||
balance_qty = sle.qty_after_transaction
|
||||
|
||||
sle.fifo_queue_qty = fifo_qty
|
||||
sle.fifo_stock_value = fifo_value
|
||||
@@ -145,9 +151,9 @@ def get_columns():
|
||||
"label": "Incoming Rate",
|
||||
},
|
||||
{
|
||||
"fieldname": "outgoing_rate",
|
||||
"fieldname": "consumption_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Outgoing Rate",
|
||||
"label": "Consumption Rate",
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_after_transaction",
|
||||
|
||||
@@ -23,9 +23,18 @@ class NegativeStockError(frappe.ValidationError): pass
|
||||
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
_exceptions = frappe.local('stockledger_exceptions')
|
||||
|
||||
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
""" Create SL entries from SL entry dicts
|
||||
|
||||
args:
|
||||
- allow_negative_stock: disable negative stock valiations if true
|
||||
- via_landed_cost_voucher: landed cost voucher cancels and reposts
|
||||
entries of purchase document. This flag is used to identify if
|
||||
cancellation and repost is happening via landed cost voucher, in
|
||||
such cases certain validations need to be ignored (like negative
|
||||
stock)
|
||||
"""
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
if sl_entries:
|
||||
cancel = sl_entries[0].get("is_cancelled")
|
||||
@@ -37,7 +46,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
future_sle_exists(args, sl_entries)
|
||||
|
||||
for sle in sl_entries:
|
||||
if sle.serial_no:
|
||||
if sle.serial_no and not via_landed_cost_voucher:
|
||||
validate_serial_no(sle)
|
||||
|
||||
if cancel:
|
||||
@@ -459,6 +468,8 @@ class update_entries_after(object):
|
||||
|
||||
# rounding as per precision
|
||||
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
|
||||
if not self.wh_data.qty_after_transaction:
|
||||
self.wh_data.stock_value = 0.0
|
||||
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
|
||||
self.wh_data.prev_stock_value = self.wh_data.stock_value
|
||||
|
||||
@@ -622,9 +633,7 @@ class update_entries_after(object):
|
||||
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
|
||||
if not allow_zero_rate:
|
||||
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
|
||||
# get rate from serial nos within same company
|
||||
@@ -690,9 +699,7 @@ class update_entries_after(object):
|
||||
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
|
||||
if not allow_zero_valuation_rate:
|
||||
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
def get_fifo_values(self, sle):
|
||||
incoming_rate = flt(sle.incoming_rate)
|
||||
@@ -723,9 +730,7 @@ class update_entries_after(object):
|
||||
# Get valuation rate from last sle if exists or from valuation rate field in item master
|
||||
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
|
||||
if not allow_zero_valuation_rate:
|
||||
_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
_rate = self.get_fallback_rate(sle)
|
||||
else:
|
||||
_rate = 0
|
||||
|
||||
@@ -788,6 +793,13 @@ class update_entries_after(object):
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_fallback_rate(self, sle) -> float:
|
||||
"""When exact incoming rate isn't available use any of other "average" rates as fallback.
|
||||
This should only get used for negative stock."""
|
||||
return get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
|
||||
def get_sle_before_datetime(self, args):
|
||||
"""get previous stock ledger entry before current time-bucket"""
|
||||
sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
|
||||
@@ -822,7 +834,7 @@ class update_entries_after(object):
|
||||
if msg_list:
|
||||
message = "\n\n".join(msg_list)
|
||||
if self.verbose:
|
||||
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
|
||||
frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
|
||||
else:
|
||||
raise NegativeStockError(message)
|
||||
|
||||
@@ -1103,7 +1115,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
|
||||
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
|
||||
|
||||
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
|
||||
frappe.throw(message, NegativeStockError, title=_('Insufficient Stock'))
|
||||
|
||||
|
||||
if not args.batch_no:
|
||||
@@ -1117,7 +1129,7 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
|
||||
frappe.get_desk_link('Warehouse', args.warehouse),
|
||||
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
|
||||
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
|
||||
frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
|
||||
frappe.throw(message, NegativeStockError, title=_("Insufficient Stock for Batch"))
|
||||
|
||||
|
||||
def get_future_sle_with_negative_qty(args):
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase):
|
||||
Test Stock and Service Item Search.
|
||||
"""
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile = make_pos_profile(name="Test POS Profile for Search")
|
||||
item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
|
||||
make_stock_entry(
|
||||
item_code="Test Search Stock Item",
|
||||
|
||||
@@ -66,6 +66,20 @@ def create_test_contact_and_address():
|
||||
contact.add_phone("+91 0000000000", is_primary_phone=True)
|
||||
contact.insert()
|
||||
|
||||
contact_two = frappe.get_doc({
|
||||
"doctype": 'Contact',
|
||||
"first_name": "_Test Contact 2 for _Test Customer",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Customer",
|
||||
"link_name": "_Test Customer"
|
||||
}
|
||||
]
|
||||
})
|
||||
contact_two.add_email("test_contact_two_customer@example.com", is_primary=True)
|
||||
contact_two.add_phone("+92 0000000000", is_primary_phone=True)
|
||||
contact_two.insert()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def change_settings(doctype, settings_dict):
|
||||
|
||||
Reference in New Issue
Block a user