mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 20:35:09 +00:00
Merge pull request #30098 from deepeshgarg007/pre_release
chore: Merge branch version-13-hotfix into version-13-pre-release
This commit is contained in:
@@ -121,6 +121,7 @@ def get_booking_dates(doc, item, posting_date=None):
|
||||
prev_gl_entry = frappe.db.sql('''
|
||||
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
|
||||
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
order by posting_date desc limit 1
|
||||
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
|
||||
@@ -228,6 +229,7 @@ def get_already_booked_amount(doc, item):
|
||||
gl_entries_details = frappe.db.sql('''
|
||||
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
|
||||
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
|
||||
and is_cancelled = 0
|
||||
group by voucher_detail_no
|
||||
'''.format(total_credit_debit, total_credit_debit_currency),
|
||||
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
|
||||
@@ -283,7 +285,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
return
|
||||
|
||||
# check if books nor frozen till endate:
|
||||
if getdate(end_date) >= getdate(accounts_frozen_upto):
|
||||
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
|
||||
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
|
||||
|
||||
if via_journal_entry:
|
||||
|
||||
@@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
frappe.scrub(row.party_type): row.party,
|
||||
"is_pos": 0,
|
||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||
"update_stock": 0,
|
||||
"update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
|
||||
"invoice_number": row.invoice_number,
|
||||
"disable_rounded_total": 1
|
||||
})
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.cache_manager import clear_doctype_cache
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
@@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
|
||||
|
||||
class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
def setUp(self):
|
||||
class TestOpeningInvoiceCreationTool(ERPNextTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self):
|
||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||
make_company()
|
||||
create_dimension()
|
||||
return super().setUpClass()
|
||||
|
||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
|
||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||
@@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
||||
return doc.make_invoices()
|
||||
|
||||
def test_opening_sales_invoice_creation(self):
|
||||
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
|
||||
try:
|
||||
invoices = self.make_invoices(company="_Test Opening Invoice Company")
|
||||
invoices = self.make_invoices(company="_Test Opening Invoice Company")
|
||||
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
self.assertEqual(len(invoices), 2)
|
||||
expected_value = {
|
||||
"keys": ["customer", "outstanding_amount", "status"],
|
||||
0: ["_Test Customer", 300, "Overdue"],
|
||||
1: ["_Test Customer 1", 250, "Overdue"],
|
||||
}
|
||||
self.check_expected_values(invoices, expected_value)
|
||||
|
||||
si = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
si = frappe.get_doc("Sales Invoice", invoices[0])
|
||||
|
||||
# Check if update stock is not enabled
|
||||
self.assertEqual(si.update_stock, 0)
|
||||
|
||||
finally:
|
||||
property_setter.delete()
|
||||
clear_doctype_cache("Sales Invoice")
|
||||
# Check if update stock is not enabled
|
||||
self.assertEqual(si.update_stock, 0)
|
||||
|
||||
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
|
||||
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
|
||||
|
||||
@@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', {
|
||||
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
|
||||
|
||||
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
|
||||
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
|
||||
(frm.doc.paid_from_account_currency != company_currency));
|
||||
|
||||
if (frm.doc.payment_type == "Pay") {
|
||||
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
|
||||
(frm.doc.paid_to_account_currency != company_currency));
|
||||
} else {
|
||||
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
|
||||
(frm.doc.paid_from_account_currency != company_currency));
|
||||
}
|
||||
|
||||
frm.toggle_display("base_received_amount", (
|
||||
frm.doc.paid_to_account_currency != company_currency
|
||||
@@ -232,7 +238,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
|
||||
|
||||
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
|
||||
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
|
||||
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
|
||||
"base_total_taxes_and_charges"], company_currency);
|
||||
|
||||
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
|
||||
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
|
||||
@@ -341,6 +348,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}
|
||||
frm.set_party_account_based_on_party = true;
|
||||
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
|
||||
args: {
|
||||
@@ -374,7 +383,11 @@ frappe.ui.form.on('Payment Entry', {
|
||||
if (r.message.bank_account) {
|
||||
frm.set_value("bank_account", r.message.bank_account);
|
||||
}
|
||||
}
|
||||
},
|
||||
() => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
|
||||
frm.doc.paid_from_account_currency, company_currency),
|
||||
() => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
|
||||
frm.doc.paid_to_account_currency, company_currency)
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -478,14 +491,14 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
paid_from_account_currency: function(frm) {
|
||||
if(!frm.doc.paid_from_account_currency) return;
|
||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
if (frm.doc.paid_from_account_currency == company_currency) {
|
||||
frm.set_value("source_exchange_rate", 1);
|
||||
} else if (frm.doc.paid_from){
|
||||
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
|
||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
@@ -505,8 +518,8 @@ frappe.ui.form.on('Payment Entry', {
|
||||
},
|
||||
|
||||
paid_to_account_currency: function(frm) {
|
||||
if(!frm.doc.paid_to_account_currency) return;
|
||||
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
|
||||
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
|
||||
frm.doc.paid_to_account_currency, company_currency);
|
||||
|
||||
@@ -66,7 +66,9 @@
|
||||
"tax_withholding_category",
|
||||
"section_break_56",
|
||||
"taxes",
|
||||
"section_break_60",
|
||||
"base_total_taxes_and_charges",
|
||||
"column_break_61",
|
||||
"total_taxes_and_charges",
|
||||
"deductions_or_loss_section",
|
||||
"deductions",
|
||||
@@ -715,12 +717,21 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Paid To Account Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_61",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_60",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-24 18:58:24.919764",
|
||||
"modified": "2022-02-23 20:08:39.559814",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
@@ -763,6 +774,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -946,8 +946,12 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
tax.base_total = tax.total * self.source_exchange_rate
|
||||
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
|
||||
if self.payment_type == 'Pay':
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
else:
|
||||
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
|
||||
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
|
||||
|
||||
if self.get('taxes'):
|
||||
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
|
||||
@@ -1078,7 +1082,7 @@ def get_outstanding_reference_documents(args):
|
||||
if d.voucher_type in ("Purchase Invoice"):
|
||||
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
|
||||
|
||||
# Get all SO / PO which are not fully billed or aginst which full advance not paid
|
||||
# Get all SO / PO which are not fully billed or against which full advance not paid
|
||||
orders_to_be_billed = []
|
||||
if (args.get("party_type") != "Student"):
|
||||
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
|
||||
|
||||
@@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase):
|
||||
self.assertEqual(flt(expected_party_balance), party_balance)
|
||||
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
|
||||
|
||||
def test_multi_currency_payment_entry_with_taxes(self):
|
||||
payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC',
|
||||
save=True)
|
||||
payment_entry.append('taxes', {
|
||||
'account_head': '_Test Account Service Tax - _TC',
|
||||
'charge_type': 'Actual',
|
||||
'tax_amount': 10,
|
||||
'add_deduct_tax': 'Add',
|
||||
'description': 'Test'
|
||||
})
|
||||
|
||||
payment_entry.save()
|
||||
self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
|
||||
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
|
||||
|
||||
def create_payment_entry(**args):
|
||||
payment_entry = frappe.new_doc('Payment Entry')
|
||||
payment_entry.company = args.get('company') or '_Test Company'
|
||||
payment_entry.payment_type = args.get('payment_type') or 'Pay'
|
||||
payment_entry.party_type = args.get('party_type') or 'Supplier'
|
||||
payment_entry.party = args.get('party') or '_Test Supplier'
|
||||
payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
|
||||
payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
|
||||
payment_entry.paid_amount = args.get('paid_amount') or 1000
|
||||
|
||||
payment_entry.setup_party_account_field()
|
||||
payment_entry.set_missing_values()
|
||||
payment_entry.set_exchange_rate()
|
||||
payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
|
||||
payment_entry.reference_no = 'Test001'
|
||||
payment_entry.reference_date = nowdate()
|
||||
|
||||
if args.get('save'):
|
||||
payment_entry.save()
|
||||
if args.get('submit'):
|
||||
payment_entry.submit()
|
||||
|
||||
return payment_entry
|
||||
|
||||
def create_payment_terms_template():
|
||||
|
||||
create_payment_term('Basic Amount Receivable')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1615,6 +1615,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
|
||||
|
||||
@@ -2366,14 +2416,22 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
|
||||
def test_sales_commission(self):
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si = frappe.copy_doc(test_records[2])
|
||||
|
||||
frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1)
|
||||
frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0)
|
||||
|
||||
item = copy.deepcopy(si.get('items')[0])
|
||||
item.update({
|
||||
"qty": 1,
|
||||
"rate": 500,
|
||||
"grant_commission": 1
|
||||
})
|
||||
si.append("items", item)
|
||||
|
||||
item = copy.deepcopy(si.get('items')[1])
|
||||
item.update({
|
||||
"qty": 1,
|
||||
"rate": 500,
|
||||
})
|
||||
|
||||
# Test valid values
|
||||
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
|
||||
|
||||
@@ -832,6 +832,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
@@ -841,7 +842,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-05 12:24:54.968907",
|
||||
"modified": "2022-02-24 14:41:36.392560",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
@@ -851,3 +852,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
|
||||
|
||||
for tax in doc.get("taxes"):
|
||||
validate_taxes_and_charges(tax)
|
||||
validate_account_head(tax, doc)
|
||||
validate_account_head(tax.idx, tax.account_head, doc.company)
|
||||
validate_cost_center(tax, doc)
|
||||
validate_inclusive_tax(tax, doc)
|
||||
|
||||
@@ -55,5 +55,8 @@ def validate_disabled(doc):
|
||||
frappe.throw(_("Disabled template must not be default template"))
|
||||
|
||||
def validate_for_tax_category(doc):
|
||||
if not doc.tax_category:
|
||||
return
|
||||
|
||||
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
|
||||
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
|
||||
|
||||
@@ -221,7 +221,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
|
||||
debit_credit_diff += flt(d.credit)
|
||||
round_off_account_exists = True
|
||||
|
||||
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
|
||||
if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
|
||||
gl_map.remove(round_off_gle)
|
||||
return
|
||||
|
||||
|
||||
@@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
|
||||
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
|
||||
|
||||
def validate_party_accounts(doc):
|
||||
|
||||
from erpnext.controllers.accounts_controller import validate_account_head
|
||||
companies = []
|
||||
|
||||
for account in doc.get("accounts"):
|
||||
@@ -331,6 +331,9 @@ def validate_party_accounts(doc):
|
||||
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
|
||||
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
|
||||
|
||||
# validate if account is mapped for same company
|
||||
validate_account_head(account.idx, account.account, account.company)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
|
||||
|
||||
@@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
|
||||
if d.parent_account:
|
||||
account = d.parent_account_name
|
||||
|
||||
# if not accounts_by_name.get(account):
|
||||
# continue
|
||||
|
||||
for company in companies:
|
||||
accounts_by_name[account][company] = \
|
||||
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
|
||||
@@ -367,7 +364,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
|
||||
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
|
||||
|
||||
def get_account_heads(root_type, companies, filters):
|
||||
accounts = get_accounts(root_type, filters)
|
||||
accounts = get_accounts(root_type, companies)
|
||||
|
||||
if not accounts:
|
||||
return None, None, None
|
||||
@@ -396,7 +393,7 @@ def update_parent_account_names(accounts):
|
||||
|
||||
for account in accounts:
|
||||
if account.parent_account:
|
||||
account["parent_account_name"] = name_to_account_map[account.parent_account]
|
||||
account["parent_account_name"] = name_to_account_map.get(account.parent_account)
|
||||
|
||||
return accounts
|
||||
|
||||
@@ -419,12 +416,19 @@ def get_subsidiary_companies(company):
|
||||
return frappe.db.sql_list("""select name from `tabCompany`
|
||||
where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt))
|
||||
|
||||
def get_accounts(root_type, filters):
|
||||
return frappe.db.sql(""" select name, is_group, company,
|
||||
parent_account, lft, rgt, root_type, report_type, account_name, account_number
|
||||
from
|
||||
`tabAccount` where company = %s and root_type = %s
|
||||
""" , (filters.get('company'), root_type), as_dict=1)
|
||||
def get_accounts(root_type, companies):
|
||||
accounts = []
|
||||
added_accounts = []
|
||||
|
||||
for company in companies:
|
||||
for account in frappe.get_all("Account", fields=["name", "is_group", "company",
|
||||
"parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"],
|
||||
filters={"company": company, "root_type": root_type}):
|
||||
if account.account_name not in added_accounts:
|
||||
accounts.append(account)
|
||||
added_accounts.append(account.account_name)
|
||||
|
||||
return accounts
|
||||
|
||||
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
|
||||
data = []
|
||||
|
||||
@@ -418,11 +418,12 @@ class Asset(AccountsController):
|
||||
def validate_asset_finance_books(self, row):
|
||||
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
|
||||
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
|
||||
.format(row.idx))
|
||||
.format(row.idx), title=_("Invalid Schedule"))
|
||||
|
||||
if not row.depreciation_start_date:
|
||||
if not self.available_for_use_date:
|
||||
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
|
||||
frappe.throw(_("Row {0}: Depreciation Start Date is required")
|
||||
.format(row.idx), title=_("Invalid Schedule"))
|
||||
row.depreciation_start_date = get_last_day(self.available_for_use_date)
|
||||
|
||||
if not self.is_existing_asset:
|
||||
@@ -440,8 +441,9 @@ class Asset(AccountsController):
|
||||
else:
|
||||
self.number_of_depreciations_booked = 0
|
||||
|
||||
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
|
||||
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
|
||||
if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
|
||||
frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
|
||||
.format(row.idx), title=_("Invalid Schedule"))
|
||||
|
||||
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
|
||||
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
|
||||
|
||||
@@ -820,8 +820,9 @@ class TestDepreciationBasics(AssetSetup):
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
def test_number_of_depreciations(self):
|
||||
"""Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations."""
|
||||
"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
|
||||
|
||||
# number_of_depreciations_booked > total_number_of_depreciations
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
calculate_depreciation = 1,
|
||||
@@ -836,6 +837,21 @@ class TestDepreciationBasics(AssetSetup):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset.save)
|
||||
|
||||
# number_of_depreciations_booked = total_number_of_depreciations
|
||||
asset_2 = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
calculate_depreciation = 1,
|
||||
available_for_use_date = "2019-12-31",
|
||||
total_number_of_depreciations = 5,
|
||||
expected_value_after_useful_life = 10000,
|
||||
depreciation_start_date = "2020-07-01",
|
||||
opening_accumulated_depreciation = 10000,
|
||||
number_of_depreciations_booked = 5,
|
||||
do_not_save = 1
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, asset_2.save)
|
||||
|
||||
def test_depreciation_start_date_is_before_purchase_date(self):
|
||||
asset = create_asset(
|
||||
item_code = "Macbook Pro",
|
||||
|
||||
@@ -68,6 +68,28 @@ frappe.ui.form.on('Asset Repair', {
|
||||
});
|
||||
|
||||
frappe.ui.form.on('Asset Repair Consumed Item', {
|
||||
item_code: function(frm, cdt, cdn) {
|
||||
var item = locals[cdt][cdn];
|
||||
|
||||
let item_args = {
|
||||
'item_code': item.item_code,
|
||||
'warehouse': frm.doc.warehouse,
|
||||
'qty': item.consumed_quantity,
|
||||
'serial_no': item.serial_no,
|
||||
'company': frm.doc.company
|
||||
};
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.utils.get_incoming_rate',
|
||||
args: {
|
||||
args: item_args
|
||||
},
|
||||
callback: function(r) {
|
||||
frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
consumed_quantity: function(frm, cdt, cdn) {
|
||||
var row = locals[cdt][cdn];
|
||||
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
|
||||
|
||||
@@ -13,12 +13,10 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fetch_from": "item.valuation_rate",
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Valuation Rate",
|
||||
"read_only": 1
|
||||
"label": "Valuation Rate"
|
||||
},
|
||||
{
|
||||
"fieldname": "consumed_quantity",
|
||||
@@ -49,7 +47,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-11 18:23:00.492483",
|
||||
"modified": "2022-02-08 17:37:20.028290",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair Consumed Item",
|
||||
|
||||
@@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
|
||||
'target_ref_field': 'stock_qty',
|
||||
'source_field': 'stock_qty'
|
||||
})
|
||||
self.status_updater.append({
|
||||
'source_dt': 'Purchase Order Item',
|
||||
'target_dt': 'Packed Item',
|
||||
'target_field': 'ordered_qty',
|
||||
'target_parent_dt': 'Sales Order',
|
||||
'target_parent_field': '',
|
||||
'join_field': 'sales_order_packed_item',
|
||||
'target_ref_field': 'qty',
|
||||
'source_field': 'stock_qty'
|
||||
})
|
||||
|
||||
def update_delivered_qty_in_sales_order(self):
|
||||
"""Update delivered qty in Sales Order for drop ship"""
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, flt, getdate, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
@@ -27,7 +27,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_make_purchase_receipt(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"material_request_item",
|
||||
"sales_order",
|
||||
"sales_order_item",
|
||||
"sales_order_packed_item",
|
||||
"supplier_quotation",
|
||||
"supplier_quotation_item",
|
||||
"col_break5",
|
||||
@@ -837,21 +838,30 @@
|
||||
"label": "Product Bundle",
|
||||
"options": "Product Bundle",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sales_order_packed_item",
|
||||
"fieldtype": "Data",
|
||||
"label": "Sales Order Packed Item",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 20:06:26.712097",
|
||||
"modified": "2022-02-02 13:10:18.398976",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"search_fields": "item_name",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import nowdate
|
||||
|
||||
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
|
||||
@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
|
||||
|
||||
|
||||
class TestRequestforQuotation(unittest.TestCase):
|
||||
class TestRequestforQuotation(FrappeTestCase):
|
||||
def test_quote_status(self):
|
||||
rfq = make_request_for_quotation()
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.accounts.party import get_due_date
|
||||
from erpnext.exceptions import PartyDisabled
|
||||
@@ -13,7 +13,7 @@ test_dependencies = ['Payment Term', 'Payment Terms Template']
|
||||
test_records = frappe.get_test_records('Supplier')
|
||||
|
||||
|
||||
class TestSupplier(unittest.TestCase):
|
||||
class TestSupplier(FrappeTestCase):
|
||||
def test_get_supplier_group_details(self):
|
||||
doc = frappe.new_doc("Supplier Group")
|
||||
doc.supplier_group_name = "_Testing Supplier Group"
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_make_purchase_order(self):
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestSupplierScorecard(unittest.TestCase):
|
||||
class TestSupplierScorecard(FrappeTestCase):
|
||||
|
||||
def test_create_scorecard(self):
|
||||
doc = make_supplier_scorecard().insert()
|
||||
@@ -49,7 +49,7 @@ valid_scorecard = [
|
||||
"min_grade":0.0,"name":"Very Poor",
|
||||
"prevent_rfqs":1,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":30.0,
|
||||
"prevent_pos":1,
|
||||
"warn_pos":0,
|
||||
@@ -65,7 +65,7 @@ valid_scorecard = [
|
||||
"name":"Poor",
|
||||
"prevent_rfqs":1,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":50.0,
|
||||
"prevent_pos":0,
|
||||
"warn_pos":0,
|
||||
@@ -81,7 +81,7 @@ valid_scorecard = [
|
||||
"name":"Average",
|
||||
"prevent_rfqs":0,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":80.0,
|
||||
"prevent_pos":0,
|
||||
"warn_pos":0,
|
||||
@@ -97,7 +97,7 @@ valid_scorecard = [
|
||||
"name":"Excellent",
|
||||
"prevent_rfqs":0,
|
||||
"notify_supplier":0,
|
||||
"doctype":"Supplier Scorecard Standing",
|
||||
"doctype":"Supplier Scorecard Scoring Standing",
|
||||
"max_grade":100.0,
|
||||
"prevent_pos":0,
|
||||
"warn_pos":0,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestSupplierScorecardCriteria(unittest.TestCase):
|
||||
class TestSupplierScorecardCriteria(FrappeTestCase):
|
||||
def test_variables_exist(self):
|
||||
delete_test_scorecards()
|
||||
for d in test_good_criteria:
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import (
|
||||
VariablePathNotFound,
|
||||
)
|
||||
|
||||
|
||||
class TestSupplierScorecardVariable(unittest.TestCase):
|
||||
class TestSupplierScorecardVariable(FrappeTestCase):
|
||||
def test_variable_exist(self):
|
||||
for d in test_existing_variables:
|
||||
my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name"))
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import unittest
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.report.procurement_tracker.procurement_tracker import execute
|
||||
@@ -14,7 +14,7 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
|
||||
class TestProcurementTracker(unittest.TestCase):
|
||||
class TestProcurementTracker(FrappeTestCase):
|
||||
def test_result_for_procurement_tracker(self):
|
||||
filters = {
|
||||
'company': '_Test Procurement Company',
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# Compiled at: 2019-05-06 09:51:46
|
||||
# Decompiled by https://python-decompiler.com
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -15,7 +15,7 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestSubcontractedItemToBeReceived(unittest.TestCase):
|
||||
class TestSubcontractedItemToBeReceived(FrappeTestCase):
|
||||
|
||||
def test_pending_and_received_qty(self):
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
# Decompiled by https://python-decompiler.com
|
||||
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
@@ -16,7 +16,7 @@ from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcont
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
|
||||
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
|
||||
class TestSubcontractedItemToBeTransferred(FrappeTestCase):
|
||||
|
||||
def test_pending_and_transferred_qty(self):
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
@@ -1567,13 +1567,12 @@ def validate_taxes_and_charges(tax):
|
||||
tax.rate = None
|
||||
|
||||
|
||||
def validate_account_head(tax, doc):
|
||||
company = frappe.get_cached_value('Account',
|
||||
tax.account_head, 'company')
|
||||
def validate_account_head(idx, account, company):
|
||||
account_company = frappe.get_cached_value('Account', account, 'company')
|
||||
|
||||
if company != doc.company:
|
||||
if account_company != company:
|
||||
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
|
||||
.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
|
||||
.format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
|
||||
|
||||
|
||||
def validate_cost_center(tax, doc):
|
||||
|
||||
@@ -507,13 +507,41 @@ class StockController(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"company": self.company
|
||||
})
|
||||
if future_sle_exists(args):
|
||||
|
||||
if future_sle_exists(args) or repost_required_for_queue(self):
|
||||
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
|
||||
if item_based_reposting:
|
||||
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||
else:
|
||||
create_repost_item_valuation_entry(args)
|
||||
|
||||
def repost_required_for_queue(doc: StockController) -> bool:
|
||||
"""check if stock document contains repeated item-warehouse with queue based valuation.
|
||||
|
||||
if queue exists for repeated items then SLEs need to reprocessed in background again.
|
||||
"""
|
||||
|
||||
consuming_sles = frappe.db.get_all("Stock Ledger Entry",
|
||||
filters={
|
||||
"voucher_type": doc.doctype,
|
||||
"voucher_no": doc.name,
|
||||
"actual_qty": ("<", 0),
|
||||
"is_cancelled": 0
|
||||
},
|
||||
fields=["item_code", "warehouse", "stock_queue"]
|
||||
)
|
||||
item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
|
||||
|
||||
unique_item_warehouses = set(item_warehouses)
|
||||
|
||||
if len(unique_item_warehouses) == len(item_warehouses):
|
||||
return False
|
||||
|
||||
for sle in consuming_sles:
|
||||
if sle.stock_queue != "[]": # using FIFO/LIFO valuation
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
|
||||
@@ -363,8 +363,6 @@ class Subcontracting():
|
||||
return
|
||||
|
||||
for row in self.get(self.raw_material_table):
|
||||
self.__validate_consumed_qty(row)
|
||||
|
||||
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
|
||||
if not self.__transferred_items or not self.__transferred_items.get(key):
|
||||
return
|
||||
@@ -372,12 +370,6 @@ class Subcontracting():
|
||||
self.__validate_batch_no(row, key)
|
||||
self.__validate_serial_no(row, key)
|
||||
|
||||
def __validate_consumed_qty(self, row):
|
||||
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
|
||||
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
|
||||
|
||||
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
|
||||
|
||||
def __validate_batch_no(self, row, key):
|
||||
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
|
||||
link = get_link_to_form('Purchase Order', row.purchase_order)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:lost_reason",
|
||||
"beta": 0,
|
||||
"creation": "2018-12-28 14:48:51.044975",
|
||||
@@ -57,7 +57,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-12-28 14:49:43.336437",
|
||||
"modified": "2022-02-16 10:49:43.336437",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity Lost Reason",
|
||||
@@ -150,4 +150,4 @@
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +265,7 @@ class ProductQuery:
|
||||
customer = get_customer(silent=True)
|
||||
if customer:
|
||||
quotation = frappe.get_all("Quotation", fields=["name"], filters=
|
||||
{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
{"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
order_by="modified desc", limit_page_length=1)
|
||||
if quotation:
|
||||
items = frappe.get_all(
|
||||
@@ -299,4 +299,4 @@ class ProductQuery:
|
||||
# slice results manually
|
||||
result[:self.page_length]
|
||||
|
||||
return result
|
||||
return result
|
||||
|
||||
@@ -311,7 +311,7 @@ def _get_cart_quotation(party=None):
|
||||
party = get_party()
|
||||
|
||||
quotation = frappe.get_all("Quotation", fields=["name"], filters=
|
||||
{"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
{"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
|
||||
order_by="modified desc", limit_page_length=1)
|
||||
|
||||
if quotation:
|
||||
|
||||
@@ -56,13 +56,19 @@ class TestShoppingCart(unittest.TestCase):
|
||||
return quotation
|
||||
|
||||
def test_get_cart_customer(self):
|
||||
self.login_as_customer()
|
||||
def validate_quotation():
|
||||
# test if quotation with customer is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(quotation.party_name, "_Test Customer")
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
return quotation
|
||||
|
||||
# test if quotation with customer is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(quotation.party_name, "_Test Customer")
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer")
|
||||
validate_quotation()
|
||||
|
||||
self.login_as_customer()
|
||||
quotation = validate_quotation()
|
||||
|
||||
return quotation
|
||||
|
||||
@@ -253,10 +259,9 @@ class TestShoppingCart(unittest.TestCase):
|
||||
self.create_user_if_not_exists("test_cart_user@example.com")
|
||||
frappe.set_user("test_cart_user@example.com")
|
||||
|
||||
def login_as_customer(self):
|
||||
self.create_user_if_not_exists("test_contact_customer@example.com",
|
||||
"_Test Contact For _Test Customer")
|
||||
frappe.set_user("test_contact_customer@example.com")
|
||||
def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"):
|
||||
self.create_user_if_not_exists(email, name)
|
||||
frappe.set_user(email)
|
||||
|
||||
def clear_existing_quotations(self):
|
||||
quotations = frappe.get_all("Quotation", filters={
|
||||
|
||||
@@ -12,7 +12,7 @@ from six.moves.urllib.parse import urlencode
|
||||
|
||||
|
||||
class GoCardlessSettings(Document):
|
||||
supported_currencies = ["EUR", "DKK", "GBP", "SEK"]
|
||||
supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
|
||||
|
||||
def validate(self):
|
||||
self.initialize_client()
|
||||
@@ -79,7 +79,7 @@ class GoCardlessSettings(Document):
|
||||
|
||||
def validate_transaction_currency(self, currency):
|
||||
if currency not in self.supported_currencies:
|
||||
frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency))
|
||||
frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
|
||||
|
||||
def get_payment_url(self, **kwargs):
|
||||
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))
|
||||
|
||||
@@ -8,10 +8,6 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_company, get_region
|
||||
|
||||
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
|
||||
SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
|
||||
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
|
||||
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
|
||||
SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
|
||||
"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
|
||||
"SE", "SI", "SK", "US"]
|
||||
@@ -35,12 +31,14 @@ def get_client():
|
||||
if api_key and api_url:
|
||||
client = taxjar.Client(api_key=api_key, api_url=api_url)
|
||||
client.set_api_config('headers', {
|
||||
'x-api-version': '2020-08-07'
|
||||
'x-api-version': '2022-01-24'
|
||||
})
|
||||
return client
|
||||
|
||||
|
||||
def create_transaction(doc, method):
|
||||
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
|
||||
|
||||
"""Create an order transaction in TaxJar"""
|
||||
|
||||
if not TAXJAR_CREATE_TRANSACTIONS:
|
||||
@@ -51,6 +49,7 @@ def create_transaction(doc, method):
|
||||
if not client:
|
||||
return
|
||||
|
||||
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
|
||||
sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
|
||||
|
||||
if not sales_tax:
|
||||
@@ -79,6 +78,7 @@ def create_transaction(doc, method):
|
||||
|
||||
def delete_transaction(doc, method):
|
||||
"""Delete an existing TaxJar order transaction"""
|
||||
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
|
||||
|
||||
if not TAXJAR_CREATE_TRANSACTIONS:
|
||||
return
|
||||
@@ -92,6 +92,8 @@ def delete_transaction(doc, method):
|
||||
|
||||
|
||||
def get_tax_data(doc):
|
||||
SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
|
||||
|
||||
from_address = get_company_address_details(doc)
|
||||
from_shipping_state = from_address.get("state")
|
||||
from_country_code = frappe.db.get_value("Country", from_address.country, "code")
|
||||
@@ -113,20 +115,20 @@ def get_tax_data(doc):
|
||||
to_shipping_state = get_state_code(to_address, 'Shipping')
|
||||
|
||||
tax_dict = {
|
||||
'from_country': from_country_code,
|
||||
'from_zip': from_address.pincode,
|
||||
'from_state': from_shipping_state,
|
||||
'from_city': from_address.city,
|
||||
'from_street': from_address.address_line1,
|
||||
'to_country': to_country_code,
|
||||
'to_zip': to_address.pincode,
|
||||
'to_city': to_address.city,
|
||||
'to_street': to_address.address_line1,
|
||||
'to_state': to_shipping_state,
|
||||
'shipping': shipping,
|
||||
'amount': doc.net_total,
|
||||
'plugin': 'erpnext',
|
||||
'line_items': line_items
|
||||
"from_country": from_country_code,
|
||||
"from_zip": from_address.pincode,
|
||||
"from_state": from_shipping_state,
|
||||
"from_city": from_address.city,
|
||||
"from_street": from_address.address_line1,
|
||||
"to_country": to_country_code,
|
||||
"to_zip": to_address.pincode,
|
||||
"to_city": to_address.city,
|
||||
"to_street": to_address.address_line1,
|
||||
"to_state": to_shipping_state,
|
||||
"shipping": shipping,
|
||||
"amount": doc.net_total,
|
||||
"plugin": "erpnext",
|
||||
"line_items": line_items
|
||||
}
|
||||
return tax_dict
|
||||
|
||||
@@ -156,6 +158,9 @@ def get_line_item_dict(item, docstatus):
|
||||
return tax_dict
|
||||
|
||||
def set_sales_tax(doc, method):
|
||||
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
|
||||
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
|
||||
|
||||
if not TAXJAR_CALCULATE_TAX:
|
||||
return
|
||||
|
||||
@@ -206,6 +211,7 @@ def set_sales_tax(doc, method):
|
||||
doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def check_for_nexus(doc, tax_dict):
|
||||
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
|
||||
if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
|
||||
for item in doc.get("items"):
|
||||
item.tax_collectable = flt(0)
|
||||
@@ -218,6 +224,8 @@ def check_for_nexus(doc, tax_dict):
|
||||
|
||||
def check_sales_tax_exemption(doc):
|
||||
# if the party is exempt from sales tax, then set all tax account heads to zero
|
||||
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
|
||||
|
||||
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
|
||||
or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
|
||||
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")
|
||||
|
||||
@@ -62,7 +62,7 @@ class JobCard(Document):
|
||||
|
||||
if self.get('time_logs'):
|
||||
for d in self.get('time_logs'):
|
||||
if get_datetime(d.from_time) > get_datetime(d.to_time):
|
||||
if d.to_time and get_datetime(d.from_time) > get_datetime(d.to_time):
|
||||
frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
|
||||
|
||||
data = self.get_overlap_for(d)
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ class ProductionPlan(Document):
|
||||
|
||||
if self.total_produced_qty > 0:
|
||||
self.status = "In Process"
|
||||
if self.check_have_work_orders_completed():
|
||||
if self.all_items_completed():
|
||||
self.status = "Completed"
|
||||
|
||||
if self.status != 'Completed':
|
||||
@@ -592,14 +592,24 @@ class ProductionPlan(Document):
|
||||
|
||||
self.append("sub_assembly_items", data)
|
||||
|
||||
def check_have_work_orders_completed(self):
|
||||
wo_status = frappe.db.get_list(
|
||||
def all_items_completed(self):
|
||||
all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
|
||||
for d in self.po_items)
|
||||
if not all_items_produced:
|
||||
return False
|
||||
|
||||
wo_status = frappe.get_all(
|
||||
"Work Order",
|
||||
filters={"production_plan": self.name},
|
||||
filters={
|
||||
"production_plan": self.name,
|
||||
"status": ("not in", ["Closed", "Stopped"]),
|
||||
"docstatus": ("<", 2),
|
||||
},
|
||||
fields="status",
|
||||
pluck="status"
|
||||
pluck="status",
|
||||
)
|
||||
return all(s == "Completed" for s in wo_status)
|
||||
all_work_orders_completed = all(s == "Completed" for s in wo_status)
|
||||
return all_work_orders_completed
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_raw_materials(doc, warehouses=None):
|
||||
@@ -1047,4 +1057,4 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
|
||||
def set_default_warehouses(row, default_warehouses):
|
||||
for field in ['wip_warehouse', 'fg_warehouse']:
|
||||
if not row.get(field):
|
||||
row[field] = default_warehouses.get(field)
|
||||
row[field] = default_warehouses.get(field)
|
||||
|
||||
@@ -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
|
||||
@@ -409,9 +410,6 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
boms = {
|
||||
"Assembly": {
|
||||
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
|
||||
"SubAssembly2": {"ChildPart3": {}},
|
||||
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
|
||||
"ChildPart5": {},
|
||||
"ChildPart6": {},
|
||||
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
|
||||
},
|
||||
@@ -469,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])
|
||||
@@ -591,6 +592,20 @@ class TestProductionPlan(ERPNextTestCase):
|
||||
pln.reload()
|
||||
self.assertEqual(pln.po_items[0].pending_qty, 1)
|
||||
|
||||
def test_qty_based_status(self):
|
||||
pp = frappe.new_doc("Production Plan")
|
||||
pp.po_items = [
|
||||
frappe._dict(planned_qty=5, produce_qty=4)
|
||||
]
|
||||
self.assertFalse(pp.all_items_completed())
|
||||
|
||||
pp.po_items = [
|
||||
frappe._dict(planned_qty=5, produce_qty=10),
|
||||
frappe._dict(planned_qty=5, produce_qty=4)
|
||||
]
|
||||
self.assertFalse(pp.all_items_completed())
|
||||
|
||||
|
||||
def create_production_plan(**args):
|
||||
"""
|
||||
sales_order (obj): Sales Order Doc Object
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1265,7 +1265,7 @@ class SalarySlip(TransactionBase):
|
||||
for i, earning in enumerate(self.earnings):
|
||||
if earning.salary_component == salary_component:
|
||||
self.earnings[i].amount = wages_amount
|
||||
self.gross_pay += self.earnings[i].amount
|
||||
self.gross_pay += flt(self.earnings[i].amount, earning.precision("amount"))
|
||||
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
|
||||
|
||||
def compute_year_to_date(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ import random
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
@@ -692,20 +693,25 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
|
||||
def make_salary_component(salary_components, test_tax, company_list=None):
|
||||
for salary_component in salary_components:
|
||||
if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
|
||||
if test_tax:
|
||||
if salary_component["type"] == "Earning":
|
||||
salary_component["is_tax_applicable"] = 1
|
||||
elif salary_component["salary_component"] == "TDS":
|
||||
salary_component["variable_based_on_taxable_salary"] = 1
|
||||
salary_component["amount_based_on_formula"] = 0
|
||||
salary_component["amount"] = 0
|
||||
salary_component["formula"] = ""
|
||||
salary_component["condition"] = ""
|
||||
salary_component["doctype"] = "Salary Component"
|
||||
salary_component["salary_component_abbr"] = salary_component["abbr"]
|
||||
frappe.get_doc(salary_component).insert()
|
||||
get_salary_component_account(salary_component["salary_component"], company_list)
|
||||
if frappe.db.exists('Salary Component', salary_component["salary_component"]):
|
||||
continue
|
||||
|
||||
if test_tax:
|
||||
if salary_component["type"] == "Earning":
|
||||
salary_component["is_tax_applicable"] = 1
|
||||
elif salary_component["salary_component"] == "TDS":
|
||||
salary_component["variable_based_on_taxable_salary"] = 1
|
||||
salary_component["amount_based_on_formula"] = 0
|
||||
salary_component["amount"] = 0
|
||||
salary_component["formula"] = ""
|
||||
salary_component["condition"] = ""
|
||||
|
||||
salary_component["salary_component_abbr"] = salary_component["abbr"]
|
||||
doc = frappe.new_doc("Salary Component")
|
||||
doc.update(salary_component)
|
||||
doc.insert()
|
||||
|
||||
get_salary_component_account(doc, company_list)
|
||||
|
||||
def get_salary_component_account(sal_comp, company_list=None):
|
||||
company = erpnext.get_default_company()
|
||||
@@ -713,7 +719,9 @@ def get_salary_component_account(sal_comp, company_list=None):
|
||||
if company_list and company not in company_list:
|
||||
company_list.append(company)
|
||||
|
||||
sal_comp = frappe.get_doc("Salary Component", sal_comp)
|
||||
if not isinstance(sal_comp, Document):
|
||||
sal_comp = frappe.get_doc("Salary Component", sal_comp)
|
||||
|
||||
if not sal_comp.get("accounts"):
|
||||
for d in company_list:
|
||||
company_abbr = frappe.get_cached_value('Company', d, 'abbr')
|
||||
|
||||
@@ -76,9 +76,6 @@ class Task(NestedSet):
|
||||
if flt(self.progress or 0) > 100:
|
||||
frappe.throw(_("Progress % for a task cannot be more than 100."))
|
||||
|
||||
if flt(self.progress) == 100:
|
||||
self.status = 'Completed'
|
||||
|
||||
if self.status == 'Completed':
|
||||
self.progress = 100
|
||||
|
||||
|
||||
@@ -151,6 +151,35 @@ class TestTimesheet(unittest.TestCase):
|
||||
settings.ignore_employee_time_overlap = initial_setting
|
||||
settings.save()
|
||||
|
||||
def test_timesheet_not_overlapping_with_continuous_timelogs(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
update_activity_type("_Test Activity Type")
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
timesheet.employee = emp
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": now_datetime(),
|
||||
"to_time": now_datetime() + datetime.timedelta(hours=3),
|
||||
"company": "_Test Company"
|
||||
}
|
||||
)
|
||||
timesheet.append(
|
||||
'time_logs',
|
||||
{
|
||||
"billable": 1,
|
||||
"activity_type": "_Test Activity Type",
|
||||
"from_time": now_datetime() + datetime.timedelta(hours=3),
|
||||
"to_time": now_datetime() + datetime.timedelta(hours=4),
|
||||
"company": "_Test Company"
|
||||
}
|
||||
)
|
||||
|
||||
timesheet.save() # should not throw an error
|
||||
|
||||
def test_to_time(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
from_time = now_datetime()
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
|
||||
from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours
|
||||
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
from erpnext.hr.utils import validate_active_employee
|
||||
@@ -145,7 +145,7 @@ class Timesheet(Document):
|
||||
if not (data.from_time and data.hours):
|
||||
return
|
||||
|
||||
_to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
|
||||
_to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True))
|
||||
if data.to_time != _to_time:
|
||||
data.to_time = _to_time
|
||||
|
||||
@@ -171,39 +171,54 @@ class Timesheet(Document):
|
||||
.format(args.idx, self.name, existing.name), OverlapError)
|
||||
|
||||
def get_overlap_for(self, fieldname, args, value):
|
||||
cond = "ts.`{0}`".format(fieldname)
|
||||
if fieldname == 'workstation':
|
||||
cond = "tsd.`{0}`".format(fieldname)
|
||||
timesheet = frappe.qb.DocType("Timesheet")
|
||||
timelog = frappe.qb.DocType("Timesheet Detail")
|
||||
|
||||
existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from
|
||||
`tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and
|
||||
(
|
||||
(%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or
|
||||
(%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or
|
||||
(%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time))
|
||||
and tsd.name!=%(name)s
|
||||
and ts.name!=%(parent)s
|
||||
and ts.docstatus < 2""".format(cond),
|
||||
{
|
||||
"val": value,
|
||||
"from_time": args.from_time,
|
||||
"to_time": args.to_time,
|
||||
"name": args.name or "No Name",
|
||||
"parent": args.parent or "No Name"
|
||||
}, as_dict=True)
|
||||
# check internal overlap
|
||||
for time_log in self.time_logs:
|
||||
if not (time_log.from_time and time_log.to_time
|
||||
and args.from_time and args.to_time): continue
|
||||
from_time = get_datetime(args.from_time)
|
||||
to_time = get_datetime(args.to_time)
|
||||
|
||||
if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \
|
||||
args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or
|
||||
(args.to_time > time_log.from_time and args.to_time < time_log.to_time) or
|
||||
(args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)):
|
||||
return self
|
||||
existing = (
|
||||
frappe.qb.from_(timesheet)
|
||||
.join(timelog)
|
||||
.on(timelog.parent == timesheet.name)
|
||||
.select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time'))
|
||||
.where(
|
||||
(timelog.name != (args.name or "No Name"))
|
||||
& (timesheet.name != (args.parent or "No Name"))
|
||||
& (timesheet.docstatus < 2)
|
||||
& (timesheet[fieldname] == value)
|
||||
& (
|
||||
((from_time > timelog.from_time) & (from_time < timelog.to_time))
|
||||
| ((to_time > timelog.from_time) & (to_time < timelog.to_time))
|
||||
| ((from_time <= timelog.from_time) & (to_time >= timelog.to_time))
|
||||
)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
if self.check_internal_overlap(fieldname, args):
|
||||
return self
|
||||
|
||||
return existing[0] if existing else None
|
||||
|
||||
def check_internal_overlap(self, fieldname, args):
|
||||
for time_log in self.time_logs:
|
||||
if not (time_log.from_time and time_log.to_time
|
||||
and args.from_time and args.to_time):
|
||||
continue
|
||||
|
||||
from_time = get_datetime(time_log.from_time)
|
||||
to_time = get_datetime(time_log.to_time)
|
||||
args_from_time = get_datetime(args.from_time)
|
||||
args_to_time = get_datetime(args.to_time)
|
||||
|
||||
if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and (
|
||||
(args_from_time > from_time and args_from_time < to_time)
|
||||
or (args_to_time > from_time and args_to_time < to_time)
|
||||
or (args_from_time <= from_time and args_to_time >= to_time)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
def update_cost(self):
|
||||
for data in self.time_logs:
|
||||
if data.activity_type or data.is_billable:
|
||||
|
||||
@@ -14,12 +14,6 @@
|
||||
"to_time",
|
||||
"hours",
|
||||
"completed",
|
||||
"section_break_7",
|
||||
"completed_qty",
|
||||
"workstation",
|
||||
"column_break_12",
|
||||
"operation",
|
||||
"operation_id",
|
||||
"project_details",
|
||||
"project",
|
||||
"project_name",
|
||||
@@ -83,43 +77,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Completed"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.work_order",
|
||||
"fieldname": "completed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Completed Qty"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.work_order",
|
||||
"fieldname": "workstation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Workstation",
|
||||
"options": "Workstation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.work_order",
|
||||
"fieldname": "operation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Operation",
|
||||
"options": "Operation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.work_order",
|
||||
"fieldname": "operation_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Operation Id"
|
||||
},
|
||||
{
|
||||
"fieldname": "project_details",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -267,7 +224,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-18 12:19:33.205940",
|
||||
"modified": "2022-02-17 16:53:34.878798",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Timesheet Detail",
|
||||
@@ -275,5 +232,6 @@
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -525,6 +525,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
item.weight_per_unit = 0;
|
||||
item.weight_uom = '';
|
||||
item.conversion_factor = 0;
|
||||
|
||||
if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
|
||||
update_stock = cint(me.frm.doc.update_stock);
|
||||
|
||||
@@ -304,12 +304,13 @@ erpnext.HierarchyChart = class {
|
||||
}
|
||||
|
||||
get_child_nodes(node_id) {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: this.method,
|
||||
method: me.method,
|
||||
args: {
|
||||
parent: node_id,
|
||||
company: this.company
|
||||
company: me.company
|
||||
}
|
||||
}).then(r => resolve(r.message));
|
||||
});
|
||||
@@ -350,12 +351,13 @@ erpnext.HierarchyChart = class {
|
||||
}
|
||||
|
||||
get_all_nodes() {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: 'erpnext.utilities.hierarchy_chart.get_all_nodes',
|
||||
args: {
|
||||
method: this.method,
|
||||
company: this.company
|
||||
method: me.method,
|
||||
company: me.company
|
||||
},
|
||||
callback: (r) => {
|
||||
resolve(r.message);
|
||||
@@ -427,8 +429,8 @@ erpnext.HierarchyChart = class {
|
||||
|
||||
add_connector(parent_id, child_id) {
|
||||
// using pure javascript for better performance
|
||||
const parent_node = document.querySelector(`#${parent_id}`);
|
||||
const child_node = document.querySelector(`#${child_id}`);
|
||||
const parent_node = document.getElementById(`${parent_id}`);
|
||||
const child_node = document.getElementById(`${child_id}`);
|
||||
|
||||
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
|
||||
|
||||
@@ -235,7 +235,7 @@ erpnext.HierarchyChartMobile = class {
|
||||
let me = this;
|
||||
return new Promise(resolve => {
|
||||
frappe.call({
|
||||
method: this.method,
|
||||
method: me.method,
|
||||
args: {
|
||||
parent: node_id,
|
||||
company: me.company,
|
||||
@@ -286,8 +286,8 @@ erpnext.HierarchyChartMobile = class {
|
||||
}
|
||||
|
||||
add_connector(parent_id, child_id) {
|
||||
const parent_node = document.querySelector(`#${parent_id}`);
|
||||
const child_node = document.querySelector(`#${child_id}`);
|
||||
const parent_node = document.getElementById(`${parent_id}`);
|
||||
const child_node = document.getElementById(`${child_id}`);
|
||||
|
||||
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
||||
|
||||
@@ -518,7 +518,8 @@ erpnext.HierarchyChartMobile = class {
|
||||
level.nextAll('li').remove();
|
||||
|
||||
let node_object = this.nodes[node.id];
|
||||
let current_node = level.find(`#${node.id}`).detach();
|
||||
let current_node = level.find(`[id="${node.id}"]`).detach();
|
||||
|
||||
current_node.removeClass('active-child active-path');
|
||||
|
||||
node_object.expanded = 0;
|
||||
|
||||
@@ -17,7 +17,7 @@ frappe.query_reports["GSTR-1"] = {
|
||||
"fieldtype": "Link",
|
||||
"options": "Address",
|
||||
"get_query": function () {
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
let company = frappe.query_report.get_filter_value('company');
|
||||
if (company) {
|
||||
return {
|
||||
"query": 'frappe.contacts.doctype.address.address.address_query',
|
||||
@@ -26,6 +26,11 @@ frappe.query_reports["GSTR-1"] = {
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "company_gstin",
|
||||
"label": __("Company GSTIN"),
|
||||
"fieldtype": "Select"
|
||||
},
|
||||
{
|
||||
"fieldname": "from_date",
|
||||
"label": __("From Date"),
|
||||
@@ -60,10 +65,21 @@ frappe.query_reports["GSTR-1"] = {
|
||||
}
|
||||
],
|
||||
onload: function (report) {
|
||||
let filters = report.get_values();
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.report.gstr_1.gstr_1.get_company_gstins',
|
||||
args: {
|
||||
company: filters.company
|
||||
},
|
||||
callback: function(r) {
|
||||
frappe.query_report.page.fields_dict.company_gstin.df.options = r.message;
|
||||
frappe.query_report.page.fields_dict.company_gstin.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
report.page.add_inner_button(__("Download as JSON"), function () {
|
||||
var filters = report.get_values();
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.report.gstr_1.gstr_1.get_json',
|
||||
args: {
|
||||
|
||||
@@ -254,7 +254,8 @@ class Gstr1Report(object):
|
||||
for opts in (("company", " and company=%(company)s"),
|
||||
("from_date", " and posting_date>=%(from_date)s"),
|
||||
("to_date", " and posting_date<=%(to_date)s"),
|
||||
("company_address", " and company_address=%(company_address)s")):
|
||||
("company_address", " and company_address=%(company_address)s"),
|
||||
("company_gstin", " and company_gstin=%(company_gstin)s")):
|
||||
if self.filters.get(opts[0]):
|
||||
conditions += opts[1]
|
||||
|
||||
@@ -1193,3 +1194,23 @@ def is_inter_state(invoice_detail):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_company_gstins(company):
|
||||
address = frappe.qb.DocType("Address")
|
||||
links = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
addresses = frappe.qb.from_(address).inner_join(links).on(
|
||||
address.name == links.parent
|
||||
).select(
|
||||
address.gstin
|
||||
).where(
|
||||
links.link_doctype == 'Company'
|
||||
).where(
|
||||
links.link_name == company
|
||||
).run(as_dict=1)
|
||||
|
||||
address_list = [''] + [d.gstin for d in addresses]
|
||||
|
||||
return address_list
|
||||
@@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
var me = this;
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Items"),
|
||||
size: "large",
|
||||
fields: [
|
||||
{
|
||||
"fieldtype": "Check",
|
||||
@@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
} else {
|
||||
let po_items = [];
|
||||
me.frm.doc.items.forEach(d => {
|
||||
let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
|
||||
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
|
||||
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
|
||||
if (pending_qty > 0) {
|
||||
po_items.push({
|
||||
"doctype": "Sales Order Item",
|
||||
@@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
get_ordered_qty: function(item, so) {
|
||||
let ordered_qty = item.ordered_qty;
|
||||
if (so.packed_items) {
|
||||
// calculate ordered qty based on packed items in case of product bundle
|
||||
let packed_items = so.packed_items.filter(
|
||||
(pi) => pi.parent_detail_docname == item.name
|
||||
);
|
||||
if (packed_items) {
|
||||
ordered_qty = packed_items.reduce(
|
||||
(sum, pi) => sum + flt(pi.ordered_qty),
|
||||
0
|
||||
);
|
||||
ordered_qty = ordered_qty / packed_items.length;
|
||||
}
|
||||
}
|
||||
return ordered_qty;
|
||||
},
|
||||
|
||||
hold_sales_order: function(){
|
||||
var me = this;
|
||||
var d = new frappe.ui.Dialog({
|
||||
|
||||
@@ -923,6 +923,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
||||
target.project = source_parent.project
|
||||
|
||||
def update_item_for_packed_item(source, target, source_parent):
|
||||
target.qty = flt(source.qty) - flt(source.ordered_qty)
|
||||
|
||||
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
||||
doc = get_mapped_doc("Sales Order", source_name, {
|
||||
"Sales Order": {
|
||||
@@ -966,6 +969,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"Packed Item": {
|
||||
"doctype": "Purchase Order Item",
|
||||
"field_map": [
|
||||
["name", "sales_order_packed_item"],
|
||||
["parent", "sales_order"],
|
||||
["uom", "uom"],
|
||||
["conversion_factor", "conversion_factor"],
|
||||
@@ -980,6 +984,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"supplier",
|
||||
"pricing_rules"
|
||||
],
|
||||
"postprocess": update_item_for_packed_item,
|
||||
"condition": lambda doc: doc.parent_item in items_to_map
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
@@ -921,6 +921,74 @@ class TestSalesOrder(ERPNextTestCase):
|
||||
self.assertEqual(purchase_orders[0].supplier, '_Test Supplier')
|
||||
self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1')
|
||||
|
||||
def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self):
|
||||
"""
|
||||
Tests if the the Product Bundles in the Items table of Sales Orders are replaced with
|
||||
their child items(from the Packed Items table) on creating a Purchase Order from it.
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
|
||||
make_product_bundle("_Test Product Bundle",
|
||||
["_Test Bundle Item 1", "_Test Bundle Item 2"])
|
||||
|
||||
so_items = [
|
||||
{
|
||||
"item_code": product_bundle.item_code,
|
||||
"warehouse": "",
|
||||
"qty": 2,
|
||||
"rate": 400,
|
||||
"delivered_by_supplier": 1,
|
||||
"supplier": '_Test Supplier'
|
||||
}
|
||||
]
|
||||
|
||||
so = make_sales_order(item_list=so_items)
|
||||
|
||||
purchase_order = make_purchase_order(so.name, selected_items=so_items)
|
||||
|
||||
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
|
||||
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
|
||||
|
||||
def test_purchase_order_updates_packed_item_ordered_qty(self):
|
||||
"""
|
||||
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
|
||||
make_product_bundle("_Test Product Bundle",
|
||||
["_Test Bundle Item 1", "_Test Bundle Item 2"])
|
||||
|
||||
so_items = [
|
||||
{
|
||||
"item_code": product_bundle.item_code,
|
||||
"warehouse": "",
|
||||
"qty": 2,
|
||||
"rate": 400,
|
||||
"delivered_by_supplier": 1,
|
||||
"supplier": '_Test Supplier'
|
||||
}
|
||||
]
|
||||
|
||||
so = make_sales_order(item_list=so_items)
|
||||
|
||||
purchase_order = make_purchase_order(so.name, selected_items=so_items)
|
||||
purchase_order.supplier = "_Test Supplier"
|
||||
purchase_order.set_warehouse = "_Test Warehouse - _TC"
|
||||
purchase_order.save()
|
||||
purchase_order.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.packed_items[0].ordered_qty, 2)
|
||||
self.assertEqual(so.packed_items[1].ordered_qty, 2)
|
||||
|
||||
def test_reserved_qty_for_closing_so(self):
|
||||
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
||||
fields=["reserved_qty"])
|
||||
|
||||
@@ -791,6 +791,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "item_code.grant_commission",
|
||||
"fieldname": "grant_commission",
|
||||
"fieldtype": "Check",
|
||||
"label": "Grant Commission",
|
||||
@@ -800,7 +801,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",
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
function get_filters() {
|
||||
let filters = [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"period_start_date",
|
||||
"label": __("Start Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
|
||||
},
|
||||
{
|
||||
"fieldname":"period_end_date",
|
||||
"label": __("End Date"),
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"sales_order",
|
||||
"label": __("Sales Order"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
"width": 100,
|
||||
"options": "Sales Order",
|
||||
"get_data": function(txt) {
|
||||
return frappe.db.get_link_options("Sales Order", txt, this.filters());
|
||||
},
|
||||
"filters": () => {
|
||||
return {
|
||||
docstatus: 1,
|
||||
payment_terms_template: ['not in', ['']],
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
transaction_date: ['between', [frappe.query_report.get_filter_value("period_start_date"), frappe.query_report.get_filter_value("period_end_date")]]
|
||||
}
|
||||
},
|
||||
on_change: function(){
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
frappe.query_reports["Payment Terms Status for Sales Order"] = {
|
||||
"filters": get_filters(),
|
||||
"formatter": function(value, row, column, data, default_formatter){
|
||||
if(column.fieldname == 'invoices' && value) {
|
||||
invoices = value.split(',');
|
||||
const invoice_formatter = (prev_value, curr_value) => {
|
||||
if(prev_value != "") {
|
||||
return prev_value + ", " + default_formatter(curr_value, row, column, data);
|
||||
}
|
||||
else {
|
||||
return default_formatter(curr_value, row, column, data);
|
||||
}
|
||||
}
|
||||
return invoices.reduce(invoice_formatter, "")
|
||||
}
|
||||
else if (column.fieldname == 'paid_amount' && value){
|
||||
formatted_value = default_formatter(value, row, column, data);
|
||||
if(value > 0) {
|
||||
formatted_value = "<span style='color:green;'>" + formatted_value + "</span>"
|
||||
}
|
||||
return formatted_value;
|
||||
}
|
||||
else if (column.fieldname == 'status' && value == 'Completed'){
|
||||
return "<span style='color:green;'>" + default_formatter(value, row, column, data) + "</span>";
|
||||
}
|
||||
|
||||
return default_formatter(value, row, column, data);
|
||||
},
|
||||
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2021-12-28 10:39:34.533964",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-12-30 10:42:06.058457",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Payment Terms Status for Sales Order",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Sales Order",
|
||||
"report_name": "Payment Terms Status for Sales Order",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
},
|
||||
{
|
||||
"role": "Maintenance User"
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
},
|
||||
{
|
||||
"role": "Stock User"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, query_builder
|
||||
from frappe.query_builder import functions
|
||||
|
||||
|
||||
def get_columns():
|
||||
columns = [
|
||||
{
|
||||
"label": _("Sales Order"),
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Order",
|
||||
},
|
||||
{
|
||||
"label": _("Posting Date"),
|
||||
"fieldname": "submitted",
|
||||
"fieldtype": "Date",
|
||||
},
|
||||
{
|
||||
"label": _("Payment Term"),
|
||||
"fieldname": "payment_term",
|
||||
"fieldtype": "Data",
|
||||
},
|
||||
{
|
||||
"label": _("Description"),
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Data",
|
||||
},
|
||||
{
|
||||
"label": _("Due Date"),
|
||||
"fieldname": "due_date",
|
||||
"fieldtype": "Date",
|
||||
},
|
||||
{
|
||||
"label": _("Invoice Portion"),
|
||||
"fieldname": "invoice_portion",
|
||||
"fieldtype": "Percent",
|
||||
},
|
||||
{
|
||||
"label": _("Payment Amount"),
|
||||
"fieldname": "base_payment_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
},
|
||||
{
|
||||
"label": _("Paid Amount"),
|
||||
"fieldname": "paid_amount",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
},
|
||||
{
|
||||
"label": _("Invoices"),
|
||||
"fieldname": "invoices",
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Invoice",
|
||||
},
|
||||
{
|
||||
"label": _("Status"),
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
},
|
||||
{
|
||||
"label": _("Currency"),
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1
|
||||
}
|
||||
]
|
||||
return columns
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
"""
|
||||
Convert filter options to conditions used in query
|
||||
"""
|
||||
filters = frappe._dict(filters) if filters else frappe._dict({})
|
||||
conditions = frappe._dict({})
|
||||
|
||||
conditions.company = filters.company or frappe.defaults.get_user_default("company")
|
||||
conditions.end_date = filters.period_end_date or frappe.utils.today()
|
||||
conditions.start_date = filters.period_start_date or frappe.utils.add_months(
|
||||
conditions.end_date, -1
|
||||
)
|
||||
conditions.sales_order = filters.sales_order or []
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_so_with_invoices(filters):
|
||||
"""
|
||||
Get Sales Order with payment terms template with their associated Invoices
|
||||
"""
|
||||
sorders = []
|
||||
|
||||
so = qb.DocType("Sales Order")
|
||||
ps = qb.DocType("Payment Schedule")
|
||||
datediff = query_builder.CustomFunction("DATEDIFF", ["cur_date", "due_date"])
|
||||
ifelse = query_builder.CustomFunction("IF", ["condition", "then", "else"])
|
||||
|
||||
conditions = get_conditions(filters)
|
||||
query_so = (
|
||||
qb.from_(so)
|
||||
.join(ps)
|
||||
.on(ps.parent == so.name)
|
||||
.select(
|
||||
so.name,
|
||||
so.transaction_date.as_("submitted"),
|
||||
ifelse(datediff(ps.due_date, functions.CurDate()) < 0, "Overdue", "Unpaid").as_("status"),
|
||||
ps.payment_term,
|
||||
ps.description,
|
||||
ps.due_date,
|
||||
ps.invoice_portion,
|
||||
ps.base_payment_amount,
|
||||
ps.paid_amount,
|
||||
)
|
||||
.where(
|
||||
(so.docstatus == 1)
|
||||
& (so.payment_terms_template != "NULL")
|
||||
& (so.company == conditions.company)
|
||||
& (so.transaction_date[conditions.start_date : conditions.end_date])
|
||||
)
|
||||
.orderby(so.name, so.transaction_date, ps.due_date)
|
||||
)
|
||||
|
||||
if conditions.sales_order != []:
|
||||
query_so = query_so.where(so.name.isin(conditions.sales_order))
|
||||
|
||||
sorders = query_so.run(as_dict=True)
|
||||
|
||||
invoices = []
|
||||
if sorders != []:
|
||||
soi = qb.DocType("Sales Order Item")
|
||||
si = qb.DocType("Sales Invoice")
|
||||
sii = qb.DocType("Sales Invoice Item")
|
||||
query_inv = (
|
||||
qb.from_(sii)
|
||||
.right_join(si)
|
||||
.on(si.name == sii.parent)
|
||||
.inner_join(soi)
|
||||
.on(soi.name == sii.so_detail)
|
||||
.select(sii.sales_order, sii.parent.as_("invoice"), si.base_grand_total.as_("invoice_amount"))
|
||||
.where((sii.sales_order.isin([x.name for x in sorders])) & (si.docstatus == 1))
|
||||
.groupby(sii.parent)
|
||||
)
|
||||
invoices = query_inv.run(as_dict=True)
|
||||
|
||||
return sorders, invoices
|
||||
|
||||
|
||||
def set_payment_terms_statuses(sales_orders, invoices, filters):
|
||||
"""
|
||||
compute status for payment terms with associated sales invoice using FIFO
|
||||
"""
|
||||
|
||||
for so in sales_orders:
|
||||
so.currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
|
||||
so.invoices = ""
|
||||
for inv in [x for x in invoices if x.sales_order == so.name and x.invoice_amount > 0]:
|
||||
if so.base_payment_amount - so.paid_amount > 0:
|
||||
amount = so.base_payment_amount - so.paid_amount
|
||||
if inv.invoice_amount >= amount:
|
||||
inv.invoice_amount -= amount
|
||||
so.paid_amount += amount
|
||||
so.invoices += "," + inv.invoice
|
||||
so.status = "Completed"
|
||||
break
|
||||
else:
|
||||
so.paid_amount += inv.invoice_amount
|
||||
inv.invoice_amount = 0
|
||||
so.invoices += "," + inv.invoice
|
||||
so.status = "Partly Paid"
|
||||
|
||||
return sales_orders, invoices
|
||||
|
||||
|
||||
def prepare_chart(s_orders):
|
||||
if len(set([x.name for x in s_orders])) == 1:
|
||||
chart = {
|
||||
"data": {
|
||||
"labels": [term.payment_term for term in s_orders],
|
||||
"datasets": [
|
||||
{"name": "Payment Amount", "values": [x.base_payment_amount for x in s_orders],},
|
||||
{"name": "Paid Amount", "values": [x.paid_amount for x in s_orders],},
|
||||
],
|
||||
},
|
||||
"type": "bar",
|
||||
}
|
||||
return chart
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns()
|
||||
sales_orders, so_invoices = get_so_with_invoices(filters)
|
||||
sales_orders, so_invoices = set_payment_terms_statuses(sales_orders, so_invoices, filters)
|
||||
|
||||
prepare_chart(sales_orders)
|
||||
|
||||
data = sales_orders
|
||||
message = []
|
||||
chart = prepare_chart(sales_orders)
|
||||
|
||||
return columns, data, message, chart
|
||||
@@ -0,0 +1,198 @@
|
||||
import datetime
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days
|
||||
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.selling.report.payment_terms_status_for_sales_order.payment_terms_status_for_sales_order import (
|
||||
execute,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.tests.utils import ERPNextTestCase
|
||||
|
||||
test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Template"]
|
||||
|
||||
|
||||
class TestPaymentTermsStatusForSalesOrder(ERPNextTestCase):
|
||||
def create_payment_terms_template(self):
|
||||
# create template for 50-50 payments
|
||||
template = None
|
||||
if frappe.db.exists("Payment Terms Template", "_Test 50-50"):
|
||||
template = frappe.get_doc("Payment Terms Template", "_Test 50-50")
|
||||
else:
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Terms Template",
|
||||
"template_name": "_Test 50-50",
|
||||
"terms": [
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term_name": "_Test 50% on 15 Days",
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 15,
|
||||
},
|
||||
{
|
||||
"doctype": "Payment Terms Template Detail",
|
||||
"due_date_based_on": "Day(s) after invoice date",
|
||||
"payment_term_name": "_Test 50% on 30 Days",
|
||||
"description": "_Test 50-50",
|
||||
"invoice_portion": 50,
|
||||
"credit_days": 30,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
template.insert()
|
||||
self.template = template
|
||||
|
||||
def test_payment_terms_status(self):
|
||||
self.create_payment_terms_template()
|
||||
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||
so = make_sales_order(
|
||||
transaction_date="2021-06-15",
|
||||
delivery_date=add_days("2021-06-15", -30),
|
||||
item=item.item_code,
|
||||
qty=10,
|
||||
rate=100000,
|
||||
do_not_save=True,
|
||||
)
|
||||
so.po_no = ""
|
||||
so.taxes_and_charges = ""
|
||||
so.taxes = ""
|
||||
so.payment_terms_template = self.template.name
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
# make invoice with 60% of the total sales order value
|
||||
sinv = make_sales_invoice(so.name)
|
||||
sinv.taxes_and_charges = ""
|
||||
sinv.taxes = ""
|
||||
sinv.items[0].qty = 6
|
||||
sinv.insert()
|
||||
sinv.submit()
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"period_start_date": "2021-06-01",
|
||||
"period_end_date": "2021-06-30",
|
||||
"sales_order": [so.name],
|
||||
}
|
||||
)
|
||||
|
||||
expected_value = [
|
||||
{
|
||||
"name": so.name,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Completed",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 6, 30),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": "INR",
|
||||
"base_payment_amount": 500000.0,
|
||||
"paid_amount": 500000.0,
|
||||
"invoices": ","+sinv.name,
|
||||
},
|
||||
{
|
||||
"name": so.name,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Partly Paid",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 7, 15),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": "INR",
|
||||
"base_payment_amount": 500000.0,
|
||||
"paid_amount": 100000.0,
|
||||
"invoices": ","+sinv.name,
|
||||
},
|
||||
]
|
||||
self.assertEqual(data, expected_value)
|
||||
|
||||
def create_exchange_rate(self, date):
|
||||
# make an entry in Currency Exchange list. serves as a static exchange rate
|
||||
if frappe.db.exists({'doctype': "Currency Exchange",'date': date,'from_currency': 'USD', 'to_currency':'INR'}):
|
||||
return
|
||||
else:
|
||||
doc = frappe.get_doc({
|
||||
'doctype': "Currency Exchange",
|
||||
'date': date,
|
||||
'from_currency': 'USD',
|
||||
'to_currency': frappe.get_cached_value("Company", '_Test Company','default_currency'),
|
||||
'exchange_rate': 70,
|
||||
'for_buying': True,
|
||||
'for_selling': True
|
||||
})
|
||||
doc.insert()
|
||||
|
||||
def test_alternate_currency(self):
|
||||
transaction_date = "2021-06-15"
|
||||
self.create_payment_terms_template()
|
||||
self.create_exchange_rate(transaction_date)
|
||||
item = create_item(item_code="_Test Excavator", is_stock_item=0)
|
||||
so = make_sales_order(
|
||||
transaction_date=transaction_date,
|
||||
currency="USD",
|
||||
delivery_date=add_days(transaction_date, -30),
|
||||
item=item.item_code,
|
||||
qty=10,
|
||||
rate=10000,
|
||||
do_not_save=True,
|
||||
)
|
||||
so.po_no = ""
|
||||
so.taxes_and_charges = ""
|
||||
so.taxes = ""
|
||||
so.payment_terms_template = self.template.name
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
# make invoice with 60% of the total sales order value
|
||||
sinv = make_sales_invoice(so.name)
|
||||
sinv.currency = "USD"
|
||||
sinv.taxes_and_charges = ""
|
||||
sinv.taxes = ""
|
||||
sinv.items[0].qty = 6
|
||||
sinv.insert()
|
||||
sinv.submit()
|
||||
columns, data, message, chart = execute(
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"period_start_date": "2021-06-01",
|
||||
"period_end_date": "2021-06-30",
|
||||
"sales_order": [so.name],
|
||||
}
|
||||
)
|
||||
|
||||
# report defaults to company currency.
|
||||
expected_value = [
|
||||
{
|
||||
"name": so.name,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Completed",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 6, 30),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
|
||||
"base_payment_amount": 3500000.0,
|
||||
"paid_amount": 3500000.0,
|
||||
"invoices": ","+sinv.name,
|
||||
},
|
||||
{
|
||||
"name": so.name,
|
||||
"submitted": datetime.date(2021, 6, 15),
|
||||
"status": "Partly Paid",
|
||||
"payment_term": None,
|
||||
"description": "_Test 50-50",
|
||||
"due_date": datetime.date(2021, 7, 15),
|
||||
"invoice_portion": 50.0,
|
||||
"currency": frappe.get_cached_value("Company", '_Test Company','default_currency'),
|
||||
"base_payment_amount": 3500000.0,
|
||||
"paid_amount": 700000.0,
|
||||
"invoices": ","+sinv.name,
|
||||
},
|
||||
]
|
||||
self.assertEqual(data, expected_value)
|
||||
@@ -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": []
|
||||
}
|
||||
@@ -594,7 +594,7 @@ $.extend(erpnext.item, {
|
||||
const increment = r.message.increment;
|
||||
|
||||
let values = [];
|
||||
for(var i = from; i <= to; i += increment) {
|
||||
for(var i = from; i <= to; i = flt(i + increment, 6)) {
|
||||
values.push(i);
|
||||
}
|
||||
attr_val_fields[d.attribute] = values;
|
||||
|
||||
@@ -399,6 +399,7 @@ class Item(Document):
|
||||
|
||||
if merge:
|
||||
self.validate_properties_before_merge(new_name)
|
||||
self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
|
||||
self.validate_duplicate_website_item_before_merge(old_name, new_name)
|
||||
|
||||
def after_rename(self, old_name, new_name, merge):
|
||||
@@ -463,6 +464,20 @@ class Item(Document):
|
||||
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
|
||||
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
|
||||
"Block merge if both old and new items have product bundles."
|
||||
old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
|
||||
new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
|
||||
|
||||
if old_bundle and new_bundle:
|
||||
bundle_link = get_link_to_form("Product Bundle", old_bundle)
|
||||
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
|
||||
|
||||
msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
|
||||
bundle_link, old_name, new_name
|
||||
)
|
||||
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def validate_duplicate_website_item_before_merge(self, old_name, new_name):
|
||||
"""
|
||||
Block merge if both old and new items have website items against them.
|
||||
@@ -480,8 +495,9 @@ class Item(Document):
|
||||
|
||||
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
|
||||
web_item_link = get_link_to_form("Website Item", old_web_item)
|
||||
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
|
||||
|
||||
msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}"
|
||||
msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
|
||||
frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
|
||||
|
||||
def set_last_purchase_rate(self, new_name):
|
||||
|
||||
@@ -14,6 +14,7 @@ from erpnext.controllers.item_variant import (
|
||||
get_variant,
|
||||
)
|
||||
from erpnext.stock.doctype.item.item import (
|
||||
DataValidationError,
|
||||
InvalidBarcode,
|
||||
StockExistsForTemplate,
|
||||
get_item_attribute,
|
||||
@@ -387,6 +388,26 @@ class TestItem(ERPNextTestCase):
|
||||
self.assertTrue(frappe.db.get_value("Bin",
|
||||
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
|
||||
|
||||
def test_item_merging_with_product_bundle(self):
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
|
||||
create_item("Test Item Bundle Item 1", is_stock_item=False)
|
||||
create_item("Test Item Bundle Item 2", is_stock_item=False)
|
||||
create_item("Test Item inside Bundle")
|
||||
bundle_items = ["Test Item inside Bundle"]
|
||||
|
||||
# make bundles for both items
|
||||
bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
|
||||
make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
|
||||
|
||||
with self.assertRaises(DataValidationError):
|
||||
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
|
||||
|
||||
bundle1.delete()
|
||||
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
|
||||
|
||||
self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
|
||||
|
||||
def test_uom_conversion_factor(self):
|
||||
if frappe.db.exists('Item', 'Test Item UOM'):
|
||||
frappe.delete_doc('Item', 'Test Item UOM')
|
||||
|
||||
@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.utils import update_gl_entries_after
|
||||
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
||||
get_gl_entries,
|
||||
make_purchase_receipt,
|
||||
@@ -177,6 +178,53 @@ class TestLandedCostVoucher(ERPNextTestCase):
|
||||
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
|
||||
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
|
||||
|
||||
def test_serialized_lcv_delivered(self):
|
||||
"""In some cases you'd want to deliver before you can know all the
|
||||
landed costs, this should be allowed for serial nos too.
|
||||
|
||||
Case:
|
||||
- receipt a serial no @ X rate
|
||||
- delivery the serial no @ X rate
|
||||
- add LCV to receipt X + Y
|
||||
- LCV should be successful
|
||||
- delivery should reflect X+Y valuation.
|
||||
"""
|
||||
serial_no = "LCV_TEST_SR_NO"
|
||||
item_code = "_Test Serialized Item"
|
||||
warehouse = "Stores - TCP1"
|
||||
|
||||
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
|
||||
warehouse=warehouse, qty=1, rate=200,
|
||||
item_code=item_code, serial_no=serial_no)
|
||||
|
||||
serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
|
||||
|
||||
# deliver it before creating LCV
|
||||
dn = create_delivery_note(item_code=item_code,
|
||||
company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
|
||||
serial_no=serial_no, qty=1, rate=500,
|
||||
cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
|
||||
|
||||
charges = 10
|
||||
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
|
||||
|
||||
new_purchase_rate = serial_no_rate + charges
|
||||
|
||||
serial_no = frappe.db.get_value("Serial No", serial_no,
|
||||
["warehouse", "purchase_rate"], as_dict=1)
|
||||
|
||||
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
|
||||
|
||||
stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
|
||||
filters={
|
||||
"voucher_no": dn.name,
|
||||
"voucher_type": dn.doctype,
|
||||
"is_cancelled": 0 # LCV cancels with same name.
|
||||
},
|
||||
fieldname="stock_value_difference")
|
||||
|
||||
# reposting should update the purchase rate in future delivery
|
||||
self.assertEqual(stock_value_difference, -new_purchase_rate)
|
||||
|
||||
def test_landed_cost_voucher_for_odd_numbers (self):
|
||||
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
|
||||
|
||||
@@ -57,14 +57,13 @@ class MaterialRequest(BuyingController):
|
||||
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
|
||||
frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no))
|
||||
|
||||
# Validate
|
||||
# ---------------------
|
||||
def validate(self):
|
||||
super(MaterialRequest, self).validate()
|
||||
|
||||
self.validate_schedule_date()
|
||||
self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_material_request_type()
|
||||
|
||||
if not self.status:
|
||||
self.status = "Draft"
|
||||
@@ -84,6 +83,12 @@ class MaterialRequest(BuyingController):
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||
|
||||
def validate_material_request_type(self):
|
||||
""" Validate fields in accordance with selected type """
|
||||
|
||||
if self.material_request_type != "Customer Provided":
|
||||
self.customer = None
|
||||
|
||||
def set_title(self):
|
||||
'''Set title as comma separated list of items'''
|
||||
if not self.title:
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"section_break_13",
|
||||
"actual_qty",
|
||||
"projected_qty",
|
||||
"ordered_qty",
|
||||
"column_break_16",
|
||||
"incoming_rate",
|
||||
"page_break",
|
||||
@@ -224,13 +225,21 @@
|
||||
"label": "Rate",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "ordered_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Ordered Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-01-28 16:03:30.780111",
|
||||
"modified": "2022-02-22 12:57:45.325488",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Packed Item",
|
||||
|
||||
@@ -162,6 +162,15 @@ class TestPurchaseReceipt(ERPNextTestCase):
|
||||
qty=abs(existing_bin_qty)
|
||||
)
|
||||
|
||||
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
|
||||
"Bin",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "_Test Warehouse - _TC"
|
||||
},
|
||||
["actual_qty", "stock_value"]
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
|
||||
@@ -9,8 +9,7 @@ from collections import defaultdict
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, floor, flt, nowdate
|
||||
from six import string_types
|
||||
from frappe.utils import cint, cstr, floor, flt, nowdate
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.utils import get_stock_balance
|
||||
@@ -75,7 +74,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
|
||||
purpose: Purpose of Stock Entry
|
||||
sync (optional): Sync with client side only for client side calls
|
||||
"""
|
||||
if isinstance(items, string_types):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
items_not_accomodated, updated_table = [], []
|
||||
@@ -143,11 +142,44 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
|
||||
if items_not_accomodated:
|
||||
show_unassigned_items_message(items_not_accomodated)
|
||||
|
||||
items[:] = updated_table if updated_table else items # modify items table
|
||||
if updated_table and _items_changed(items, updated_table, doctype):
|
||||
items[:] = updated_table
|
||||
frappe.msgprint(_("Applied putaway rules."), alert=True)
|
||||
|
||||
if sync and json.loads(sync): # sync with client side
|
||||
return items
|
||||
|
||||
def _items_changed(old, new, doctype: str) -> bool:
|
||||
""" Check if any items changed by application of putaway rules.
|
||||
|
||||
If not, changing item table can have side effects since `name` items also changes.
|
||||
"""
|
||||
if len(old) != len(new):
|
||||
return True
|
||||
|
||||
old = [frappe._dict(item) if isinstance(item, dict) else item for item in old]
|
||||
|
||||
if doctype == "Stock Entry":
|
||||
compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no")
|
||||
sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa
|
||||
flt(item.transfer_qty), cstr(item.serial_no))
|
||||
else:
|
||||
# purchase receipt / invoice
|
||||
compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no")
|
||||
sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa
|
||||
flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no))
|
||||
|
||||
old_sorted = sorted(old, key=sort_key)
|
||||
new_sorted = sorted(new, key=sort_key)
|
||||
|
||||
# Once sorted by all relevant keys both tables should align if they are same.
|
||||
for old_item, new_item in zip(old_sorted, new_sorted):
|
||||
for key in compare_keys:
|
||||
if old_item.get(key) != new_item.get(key):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
|
||||
"""Returns an ordered list of putaway rules to apply on an item."""
|
||||
filters = {
|
||||
|
||||
@@ -35,6 +35,18 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
new_uom.uom_name = "Bag"
|
||||
new_uom.save()
|
||||
|
||||
def assertUnchangedItemsOnResave(self, doc):
|
||||
""" Check if same items remain even after reapplication of rules.
|
||||
|
||||
This is required since some business logic like subcontracting
|
||||
depends on `name` of items to be same if item isn't changed.
|
||||
"""
|
||||
doc.reload()
|
||||
old_items = {d.name for d in doc.items}
|
||||
doc.save()
|
||||
new_items = {d.name for d in doc.items}
|
||||
self.assertSetEqual(old_items, new_items)
|
||||
|
||||
def test_putaway_rules_priority(self):
|
||||
"""Test if rule is applied by priority, irrespective of free space."""
|
||||
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
|
||||
@@ -50,6 +62,8 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
self.assertEqual(pr.items[1].qty, 100)
|
||||
self.assertEqual(pr.items[1].warehouse, self.warehouse_2)
|
||||
|
||||
self.assertUnchangedItemsOnResave(pr)
|
||||
|
||||
pr.delete()
|
||||
rule_1.delete()
|
||||
rule_2.delete()
|
||||
@@ -162,6 +176,8 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
# leftover space was for 500 kg (0.5 Bag)
|
||||
# Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
|
||||
|
||||
self.assertUnchangedItemsOnResave(pr)
|
||||
|
||||
pr.delete()
|
||||
rule_1.delete()
|
||||
rule_2.delete()
|
||||
@@ -196,6 +212,8 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
|
||||
self.assertEqual(pr.items[1].putaway_rule, rule_1.name)
|
||||
|
||||
self.assertUnchangedItemsOnResave(pr)
|
||||
|
||||
pr.delete()
|
||||
rule_1.delete()
|
||||
|
||||
@@ -239,6 +257,8 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg
|
||||
self.assertEqual(stock_entry_item.putaway_rule, rule_2.name)
|
||||
|
||||
self.assertUnchangedItemsOnResave(stock_entry)
|
||||
|
||||
stock_entry.delete()
|
||||
rule_1.delete()
|
||||
rule_2.delete()
|
||||
@@ -294,6 +314,8 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
self.assertEqual(stock_entry.items[2].qty, 200)
|
||||
self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name)
|
||||
|
||||
self.assertUnchangedItemsOnResave(stock_entry)
|
||||
|
||||
stock_entry.delete()
|
||||
rule_1.delete()
|
||||
rule_2.delete()
|
||||
@@ -344,6 +366,8 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:]))
|
||||
self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
|
||||
|
||||
self.assertUnchangedItemsOnResave(stock_entry)
|
||||
|
||||
stock_entry.delete()
|
||||
pr.cancel()
|
||||
rule_1.delete()
|
||||
@@ -366,6 +390,8 @@ class TestPutawayRule(ERPNextTestCase):
|
||||
self.assertEqual(stock_entry_item.qty, 100)
|
||||
self.assertEqual(stock_entry_item.putaway_rule, rule_1.name)
|
||||
|
||||
self.assertUnchangedItemsOnResave(stock_entry)
|
||||
|
||||
stock_entry.delete()
|
||||
rule_1.delete()
|
||||
rule_2.delete()
|
||||
|
||||
@@ -627,6 +627,12 @@ frappe.ui.form.on('Stock Entry Detail', {
|
||||
frm.events.set_serial_no(frm, cdt, cdn, () => {
|
||||
frm.events.get_warehouse_details(frm, cdt, cdn);
|
||||
});
|
||||
|
||||
// set allow_zero_valuation_rate to 0 if s_warehouse is selected.
|
||||
let item = frappe.get_doc(cdt, cdn);
|
||||
if (item.s_warehouse) {
|
||||
item.allow_zero_valuation_rate = 0;
|
||||
}
|
||||
},
|
||||
|
||||
t_warehouse: function(frm, cdt, cdn) {
|
||||
|
||||
@@ -45,6 +45,7 @@ def get_sle(**args):
|
||||
|
||||
class TestStockEntry(ERPNextTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
|
||||
|
||||
@@ -566,6 +567,7 @@ class TestStockEntry(ERPNextTestCase):
|
||||
st1.set_stock_entry_type()
|
||||
st1.insert()
|
||||
st1.submit()
|
||||
st1.cancel()
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
|
||||
@@ -690,6 +692,8 @@ class TestStockEntry(ERPNextTestCase):
|
||||
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
|
||||
"is_default": 1, "docstatus": 1})
|
||||
|
||||
make_item_variant() # make variant of _Test Variant Item if absent
|
||||
|
||||
work_order = frappe.new_doc("Work Order")
|
||||
work_order.update({
|
||||
"company": "_Test Company",
|
||||
@@ -1101,13 +1105,10 @@ class TestStockEntry(ERPNextTestCase):
|
||||
|
||||
# Check if FG cost is calculated based on RM total cost
|
||||
# RM total cost = 200, FG rate = 200/4(FG qty) = 50
|
||||
self.assertEqual(se.items[1].basic_rate, 50)
|
||||
self.assertEqual(se.items[1].basic_rate, flt(se.items[0].basic_rate/4))
|
||||
self.assertEqual(se.value_difference, 0.0)
|
||||
self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
|
||||
|
||||
# teardown
|
||||
se.delete()
|
||||
|
||||
def make_serialized_item(**args):
|
||||
args = frappe._dict(args)
|
||||
se = frappe.copy_doc(test_records[0])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-03-29 18:22:12",
|
||||
"creation": "2022-02-05 00:17:49.860824",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Other",
|
||||
"editable_grid": 1,
|
||||
@@ -340,13 +340,13 @@
|
||||
"label": "More Information"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"fieldname": "allow_zero_valuation_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Zero Valuation Rate",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
"print_hide": 1,
|
||||
"read_only_depends_on": "eval:doc.s_warehouse"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
@@ -556,12 +556,14 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-22 16:47:11.268975",
|
||||
"modified": "2022-02-26 00:51:24.963653",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
"naming_rule": "Random",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC"
|
||||
"sort_order": "ASC",
|
||||
"states": []
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe.core.page.permission_manager.permission_manager import reset
|
||||
from frappe.utils import add_days, today
|
||||
@@ -32,6 +34,27 @@ class TestStockLedgerEntry(ERPNextTestCase):
|
||||
frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
|
||||
frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
|
||||
|
||||
|
||||
def assertSLEs(self, doc, expected_sles, sle_filters=None):
|
||||
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
|
||||
|
||||
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
|
||||
if sle_filters:
|
||||
filters.update(sle_filters)
|
||||
sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
|
||||
order_by="timestamp(posting_date, posting_time), creation")
|
||||
|
||||
for exp_sle, act_sle in zip(expected_sles, sles):
|
||||
for k, v in exp_sle.items():
|
||||
act_value = act_sle[k]
|
||||
if k == "stock_queue":
|
||||
act_value = json.loads(act_value)
|
||||
if act_value and act_value[0][0] == 0:
|
||||
# ignore empty fifo bins
|
||||
continue
|
||||
|
||||
self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
|
||||
|
||||
def test_item_cost_reposting(self):
|
||||
company = "_Test Company"
|
||||
|
||||
@@ -349,6 +372,77 @@ class TestStockLedgerEntry(ERPNextTestCase):
|
||||
frappe.set_user("Administrator")
|
||||
user.remove_roles("Stock Manager")
|
||||
|
||||
def test_fifo_dependent_consumption(self):
|
||||
item = make_item("_TestFifoTransferRates")
|
||||
source = "_Test Warehouse - _TC"
|
||||
target = "Stores - _TC"
|
||||
|
||||
rates = [10 * i for i in range(1, 20)]
|
||||
|
||||
receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||
row.basic_rate = rate
|
||||
receipt.append("items", row)
|
||||
|
||||
receipt.save()
|
||||
receipt.submit()
|
||||
|
||||
expected_queues = []
|
||||
for idx, rate in enumerate(rates, start=1):
|
||||
expected_queues.append(
|
||||
{"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
|
||||
)
|
||||
self.assertSLEs(receipt, expected_queues)
|
||||
|
||||
transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
|
||||
transfer.append("items", row)
|
||||
|
||||
transfer.save()
|
||||
transfer.submit()
|
||||
|
||||
# same exact queue should be transferred
|
||||
self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
|
||||
|
||||
def test_fifo_multi_item_repack_consumption(self):
|
||||
rm = make_item("_TestFifoRepackRM")
|
||||
packed = make_item("_TestFifoRepackFinished")
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
rates = [10 * i for i in range(1, 5)]
|
||||
|
||||
receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
|
||||
row.basic_rate = rate
|
||||
receipt.append("items", row)
|
||||
|
||||
receipt.save()
|
||||
receipt.submit()
|
||||
|
||||
repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
|
||||
do_not_save=True, rate=10, purpose="Repack")
|
||||
for rate in rates[1:]:
|
||||
row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
|
||||
repack.append("items", row)
|
||||
|
||||
repack.append("items", {
|
||||
"item_code": packed.name,
|
||||
"t_warehouse": warehouse,
|
||||
"qty": 1,
|
||||
"transfer_qty": 1,
|
||||
})
|
||||
|
||||
repack.save()
|
||||
repack.submit()
|
||||
|
||||
# same exact queue should be transferred
|
||||
self.assertSLEs(repack, [
|
||||
{"incoming_rate": sum(rates) * 10}
|
||||
], sle_filters={"item_code": packed.name})
|
||||
|
||||
|
||||
def create_repack_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -21,6 +21,7 @@ SLE_FIELDS = (
|
||||
"stock_value",
|
||||
"stock_value_difference",
|
||||
"valuation_rate",
|
||||
"voucher_detail_no",
|
||||
)
|
||||
|
||||
|
||||
@@ -60,10 +61,15 @@ def add_invariant_check_fields(sles):
|
||||
fifo_qty += qty
|
||||
fifo_value += qty * rate
|
||||
|
||||
if sle.actual_qty < 0:
|
||||
sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
|
||||
|
||||
balance_qty += sle.actual_qty
|
||||
balance_stock_value += sle.stock_value_difference
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||
balance_qty = sle.qty_after_transaction
|
||||
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
|
||||
if balance_qty is None:
|
||||
balance_qty = sle.qty_after_transaction
|
||||
|
||||
sle.fifo_queue_qty = fifo_qty
|
||||
sle.fifo_stock_value = fifo_value
|
||||
@@ -145,9 +151,9 @@ def get_columns():
|
||||
"label": "Incoming Rate",
|
||||
},
|
||||
{
|
||||
"fieldname": "outgoing_rate",
|
||||
"fieldname": "consumption_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Outgoing Rate",
|
||||
"label": "Consumption Rate",
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_after_transaction",
|
||||
|
||||
@@ -23,9 +23,18 @@ class NegativeStockError(frappe.ValidationError): pass
|
||||
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
_exceptions = frappe.local('stockledger_exceptions')
|
||||
|
||||
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
""" Create SL entries from SL entry dicts
|
||||
|
||||
args:
|
||||
- allow_negative_stock: disable negative stock valiations if true
|
||||
- via_landed_cost_voucher: landed cost voucher cancels and reposts
|
||||
entries of purchase document. This flag is used to identify if
|
||||
cancellation and repost is happening via landed cost voucher, in
|
||||
such cases certain validations need to be ignored (like negative
|
||||
stock)
|
||||
"""
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
if sl_entries:
|
||||
cancel = sl_entries[0].get("is_cancelled")
|
||||
@@ -37,7 +46,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
|
||||
future_sle_exists(args, sl_entries)
|
||||
|
||||
for sle in sl_entries:
|
||||
if sle.serial_no:
|
||||
if sle.serial_no and not via_landed_cost_voucher:
|
||||
validate_serial_no(sle)
|
||||
|
||||
if cancel:
|
||||
@@ -459,6 +468,8 @@ class update_entries_after(object):
|
||||
|
||||
# rounding as per precision
|
||||
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
|
||||
if not self.wh_data.qty_after_transaction:
|
||||
self.wh_data.stock_value = 0.0
|
||||
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
|
||||
self.wh_data.prev_stock_value = self.wh_data.stock_value
|
||||
|
||||
@@ -622,9 +633,7 @@ class update_entries_after(object):
|
||||
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
|
||||
if not allow_zero_rate:
|
||||
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
|
||||
# get rate from serial nos within same company
|
||||
@@ -690,9 +699,7 @@ class update_entries_after(object):
|
||||
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
|
||||
if not allow_zero_valuation_rate:
|
||||
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
def get_fifo_values(self, sle):
|
||||
incoming_rate = flt(sle.incoming_rate)
|
||||
@@ -723,9 +730,7 @@ class update_entries_after(object):
|
||||
# Get valuation rate from last sle if exists or from valuation rate field in item master
|
||||
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
|
||||
if not allow_zero_valuation_rate:
|
||||
_rate = get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
_rate = self.get_fallback_rate(sle)
|
||||
else:
|
||||
_rate = 0
|
||||
|
||||
@@ -788,6 +793,13 @@ class update_entries_after(object):
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_fallback_rate(self, sle) -> float:
|
||||
"""When exact incoming rate isn't available use any of other "average" rates as fallback.
|
||||
This should only get used for negative stock."""
|
||||
return get_valuation_rate(sle.item_code, sle.warehouse,
|
||||
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
|
||||
currency=erpnext.get_company_currency(sle.company), company=sle.company)
|
||||
|
||||
def get_sle_before_datetime(self, args):
|
||||
"""get previous stock ledger entry before current time-bucket"""
|
||||
sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase):
|
||||
Test Stock and Service Item Search.
|
||||
"""
|
||||
|
||||
pos_profile = make_pos_profile()
|
||||
pos_profile = make_pos_profile(name="Test POS Profile for Search")
|
||||
item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
|
||||
make_stock_entry(
|
||||
item_code="Test Search Stock Item",
|
||||
|
||||
@@ -66,6 +66,20 @@ def create_test_contact_and_address():
|
||||
contact.add_phone("+91 0000000000", is_primary_phone=True)
|
||||
contact.insert()
|
||||
|
||||
contact_two = frappe.get_doc({
|
||||
"doctype": 'Contact',
|
||||
"first_name": "_Test Contact 2 for _Test Customer",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Customer",
|
||||
"link_name": "_Test Customer"
|
||||
}
|
||||
]
|
||||
})
|
||||
contact_two.add_email("test_contact_two_customer@example.com", is_primary=True)
|
||||
contact_two.add_phone("+92 0000000000", is_primary_phone=True)
|
||||
contact_two.insert()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def change_settings(doctype, settings_dict):
|
||||
|
||||
@@ -3726,7 +3726,7 @@ Earliest Age,Frühestes Alter,
|
||||
Edit Details,Details bearbeiten,
|
||||
Edit Profile,Profil bearbeiten,
|
||||
Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich,
|
||||
Email,Email,
|
||||
Email,E-Mail,
|
||||
Email Campaigns,E-Mail-Kampagnen,
|
||||
Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
|
||||
Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
|
||||
@@ -6481,7 +6481,7 @@ Select Users,Wählen Sie Benutzer aus,
|
||||
Send Emails At,Die E-Mails senden um,
|
||||
Reminder,Erinnerung,
|
||||
Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
|
||||
email,Email,
|
||||
email,E-Mail,
|
||||
Parent Department,Elternabteilung,
|
||||
Leave Block List,Urlaubssperrenliste,
|
||||
Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -11,7 +11,7 @@
|
||||
{% if frappe.session.user == 'Guest' %}
|
||||
<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
|
||||
{% elif not has_access %}
|
||||
<button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()" disabled>{{_('Enroll')}}</button>
|
||||
<button id="enroll" class="btn btn-primary btn-lg" onclick="enroll()">{{_('Enroll')}}</button>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
@@ -20,34 +20,35 @@
|
||||
<script type="text/javascript">
|
||||
frappe.ready(() => {
|
||||
btn = document.getElementById('enroll');
|
||||
if (btn) btn.disabled = false;
|
||||
})
|
||||
|
||||
function enroll() {
|
||||
let params = frappe.utils.get_query_params()
|
||||
|
||||
let btn = document.getElementById('enroll');
|
||||
btn.disbaled = true;
|
||||
btn.innerText = __('Enrolling...')
|
||||
|
||||
let opts = {
|
||||
method: 'erpnext.education.utils.enroll_in_program',
|
||||
args: {
|
||||
program_name: params.program
|
||||
}
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Enrolling...')
|
||||
}
|
||||
|
||||
frappe.call(opts).then(res => {
|
||||
let success_dialog = new frappe.ui.Dialog({
|
||||
title: __('Success'),
|
||||
primary_action_label: __('View Program Content'),
|
||||
primary_action: function() {
|
||||
window.location.reload();
|
||||
},
|
||||
secondary_action: function() {
|
||||
window.location.reload()
|
||||
window.location.reload();
|
||||
}
|
||||
})
|
||||
success_dialog.set_message(__('You have successfully enrolled for the program '));
|
||||
success_dialog.$message.show()
|
||||
success_dialog.show();
|
||||
btn.disbaled = false;
|
||||
success_dialog.set_message(__('You have successfully enrolled for the program '));
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user