Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-29740

This commit is contained in:
Deepesh Garg
2022-03-08 21:41:53 +05:30
committed by GitHub
77 changed files with 1221 additions and 308 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"),

View File

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

View File

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

View File

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

View File

@@ -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`")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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'):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ def get_data():
'transactions': [
{
'label': _('Manufacture'),
'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet']
'items': ['BOM', 'Work Order', 'Job Card']
}
]
}

View File

@@ -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"));
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',]
}

View File

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

View File

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

View File

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

View File

@@ -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
]);
}
}
});

View File

@@ -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');

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"])

View File

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

View File

@@ -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"
});
}
});

View File

@@ -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": []
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": []
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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