Merge pull request #30098 from deepeshgarg007/pre_release

chore: Merge branch version-13-hotfix into version-13-pre-release
This commit is contained in:
Deepesh Garg
2022-03-07 19:12:42 +05:30
committed by GitHub
85 changed files with 1556 additions and 321 deletions

View File

@@ -121,6 +121,7 @@ def get_booking_dates(doc, item, posting_date=None):
prev_gl_entry = frappe.db.sql(''' prev_gl_entry = frappe.db.sql('''
select name, posting_date from `tabGL Entry` where company=%s and account=%s and 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 voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1 order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) ''', (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(''' gl_entries_details = frappe.db.sql('''
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no 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 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 group by voucher_detail_no
'''.format(total_credit_debit, total_credit_debit_currency), '''.format(total_credit_debit, total_credit_debit_currency),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) (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 return
# check if books nor frozen till endate: # 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)) end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry: if via_journal_entry:

View File

@@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party, frappe.scrub(row.party_type): row.party,
"is_pos": 0, "is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "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, "invoice_number": row.invoice_number,
"disable_rounded_total": 1 "disable_rounded_total": 1
}) })

View File

@@ -1,11 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe 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 ( from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension, 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 ( from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account, get_temporary_opening_account,
) )
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase): class TestOpeningInvoiceCreationTool(ERPNextTestCase):
def setUp(self): @classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"): if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company() make_company()
create_dimension() 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): 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") doc = frappe.get_single("Opening Invoice Creation Tool")
@@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
return doc.make_invoices() return doc.make_invoices()
def test_opening_sales_invoice_creation(self): def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") invoices = self.make_invoices(company="_Test Opening Invoice Company")
try:
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
"keys": ["customer", "outstanding_amount", "status"], "keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"], 0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"], 1: ["_Test Customer 1", 250, "Overdue"],
} }
self.check_expected_values(invoices, expected_value) 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 # Check if update stock is not enabled
self.assertEqual(si.update_stock, 0) self.assertEqual(si.update_stock, 0)
finally:
property_setter.delete()
clear_doctype_cache("Sales Invoice")
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"

View File

@@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)); frm.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_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.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency 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: ""; 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", 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(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_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; frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
return frappe.call({ return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details", method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: { args: {
@@ -374,7 +383,11 @@ frappe.ui.form.on('Payment Entry', {
if (r.message.bank_account) { if (r.message.bank_account) {
frm.set_value("bank_account", 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) { paid_from_account_currency: function(frm) {
if(!frm.doc.paid_from_account_currency) return; if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) { if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1); frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){ } else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) { 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({ frappe.call({
method: "erpnext.setup.utils.get_exchange_rate", method: "erpnext.setup.utils.get_exchange_rate",
args: { args: {
@@ -505,8 +518,8 @@ frappe.ui.form.on('Payment Entry', {
}, },
paid_to_account_currency: function(frm) { paid_to_account_currency: function(frm) {
if(!frm.doc.paid_to_account_currency) return; if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate", frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency); frm.doc.paid_to_account_currency, company_currency);

View File

@@ -66,7 +66,9 @@
"tax_withholding_category", "tax_withholding_category",
"section_break_56", "section_break_56",
"taxes", "taxes",
"section_break_60",
"base_total_taxes_and_charges", "base_total_taxes_and_charges",
"column_break_61",
"total_taxes_and_charges", "total_taxes_and_charges",
"deductions_or_loss_section", "deductions_or_loss_section",
"deductions", "deductions",
@@ -715,12 +717,21 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Paid To Account Type" "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, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-11-24 18:58:24.919764", "modified": "2022-02-23 20:08:39.559814",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",
@@ -763,6 +774,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -946,8 +946,12 @@ class PaymentEntry(AccountsController):
tax.base_total = tax.total * self.source_exchange_rate tax.base_total = tax.total * self.source_exchange_rate
self.total_taxes_and_charges += current_tax_amount if self.payment_type == 'Pay':
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate 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'): if self.get('taxes'):
self.paid_amount_after_tax = self.get('taxes')[-1].base_total 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"): if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") 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 = [] orders_to_be_billed = []
if (args.get("party_type") != "Student"): if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),

View File

@@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_balance), party_balance) self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_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(): def create_payment_terms_template():
create_payment_term('Basic Amount Receivable') create_payment_term('Basic Amount Receivable')

View File

@@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True):
'to_date': doc.to_date, 'to_date': doc.to_date,
'company': doc.company, 'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None, '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_type': 'Customer',
'party': [entry.customer], 'party': [entry.customer],
'presentation_currency': presentation_currency, 'presentation_currency': presentation_currency,

View File

@@ -1615,6 +1615,56 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit) self.assertEqual(expected_values[gle.account][2], gle.credit)
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): def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule 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): 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 = copy.deepcopy(si.get('items')[0])
item.update({ item.update({
"qty": 1, "qty": 1,
"rate": 500, "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 # Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):

View File

@@ -832,6 +832,7 @@
}, },
{ {
"default": "0", "default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission", "fieldname": "grant_commission",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Grant Commission", "label": "Grant Commission",
@@ -841,7 +842,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-10-05 12:24:54.968907", "modified": "2022-02-24 14:41:36.392560",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Item", "name": "Sales Invoice Item",
@@ -851,3 +852,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC"
} }

View File

@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"): for tax in doc.get("taxes"):
validate_taxes_and_charges(tax) 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_cost_center(tax, doc)
validate_inclusive_tax(tax, doc) validate_inclusive_tax(tax, doc)
@@ -55,5 +55,8 @@ def validate_disabled(doc):
frappe.throw(_("Disabled template must not be default template")) frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc): 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]}): 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))) frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))

View File

@@ -221,7 +221,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
debit_credit_diff += flt(d.credit) debit_credit_diff += flt(d.credit)
round_off_account_exists = True 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) gl_map.remove(round_off_gle)
return return

View File

@@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc): def validate_party_accounts(doc):
from erpnext.controllers.accounts_controller import validate_account_head
companies = [] companies = []
for account in doc.get("accounts"): 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: 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")) 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() @frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None): def get_due_date(posting_date, party_type, party, company=None, bill_date=None):

View File

@@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account: if d.parent_account:
account = d.parent_account_name account = d.parent_account_name
# if not accounts_by_name.get(account):
# continue
for company in companies: for company in companies:
accounts_by_name[account][company] = \ accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) 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) accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters): def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, companies)
if not accounts: if not accounts:
return None, None, None return None, None, None
@@ -396,7 +393,7 @@ def update_parent_account_names(accounts):
for account in accounts: for account in accounts:
if account.parent_account: 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 return accounts
@@ -419,12 +416,19 @@ def get_subsidiary_companies(company):
return frappe.db.sql_list("""select name from `tabCompany` return frappe.db.sql_list("""select name from `tabCompany`
where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt)) where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt))
def get_accounts(root_type, filters): def get_accounts(root_type, companies):
return frappe.db.sql(""" select name, is_group, company, accounts = []
parent_account, lft, rgt, root_type, report_type, account_name, account_number added_accounts = []
from
`tabAccount` where company = %s and root_type = %s for company in companies:
""" , (filters.get('company'), root_type), as_dict=1) 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): def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = [] data = []

View File

@@ -418,11 +418,12 @@ class Asset(AccountsController):
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): 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") 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 row.depreciation_start_date:
if not self.available_for_use_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) row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset: if not self.is_existing_asset:
@@ -440,8 +441,9 @@ class Asset(AccountsController):
else: else:
self.number_of_depreciations_booked = 0 self.number_of_depreciations_booked = 0
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations): if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations")) 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): 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") frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")

View File

@@ -820,8 +820,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self): 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( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",
calculate_depreciation = 1, calculate_depreciation = 1,
@@ -836,6 +837,21 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) 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): def test_depreciation_start_date_is_before_purchase_date(self):
asset = create_asset( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",

View File

@@ -68,6 +68,28 @@ frappe.ui.form.on('Asset Repair', {
}); });
frappe.ui.form.on('Asset Repair Consumed Item', { 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) { consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn]; var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate); frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);

View File

@@ -13,12 +13,10 @@
], ],
"fields": [ "fields": [
{ {
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate", "fieldname": "valuation_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Valuation Rate", "label": "Valuation Rate"
"read_only": 1
}, },
{ {
"fieldname": "consumed_quantity", "fieldname": "consumed_quantity",
@@ -49,7 +47,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-11-11 18:23:00.492483", "modified": "2022-02-08 17:37:20.028290",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Repair Consumed Item", "name": "Asset Repair Consumed Item",

View File

@@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
'target_ref_field': 'stock_qty', 'target_ref_field': 'stock_qty',
'source_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): def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship""" """Update delivered qty in Sales Order for drop ship"""

View File

@@ -3,9 +3,9 @@
import json import json
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, getdate, nowdate from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry 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 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): def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True) po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name) self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)

View File

@@ -63,6 +63,7 @@
"material_request_item", "material_request_item",
"sales_order", "sales_order",
"sales_order_item", "sales_order_item",
"sales_order_packed_item",
"supplier_quotation", "supplier_quotation",
"supplier_quotation_item", "supplier_quotation_item",
"col_break5", "col_break5",
@@ -837,21 +838,30 @@
"label": "Product Bundle", "label": "Product Bundle",
"options": "Product Bundle", "options": "Product Bundle",
"read_only": 1 "read_only": 1
},
{
"fieldname": "sales_order_packed_item",
"fieldtype": "Data",
"label": "Sales Order Packed Item",
"no_copy": 1,
"print_hide": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-30 20:06:26.712097", "modified": "2022-02-02 13:10:18.398976",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"search_fields": "item_name", "search_fields": "item_name",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( 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 from erpnext.templates.pages.rfq import check_supplier_has_docname_access
class TestRequestforQuotation(unittest.TestCase): class TestRequestforQuotation(FrappeTestCase):
def test_quote_status(self): def test_quote_status(self):
rfq = make_request_for_quotation() rfq = make_request_for_quotation()

View File

@@ -1,10 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.party import get_due_date from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled from erpnext.exceptions import PartyDisabled
@@ -13,7 +13,7 @@ test_dependencies = ['Payment Term', 'Payment Terms Template']
test_records = frappe.get_test_records('Supplier') test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase): class TestSupplier(FrappeTestCase):
def test_get_supplier_group_details(self): def test_get_supplier_group_details(self):
doc = frappe.new_doc("Supplier Group") doc = frappe.new_doc("Supplier Group")
doc.supplier_group_name = "_Testing Supplier Group" doc.supplier_group_name = "_Testing Supplier Group"

View File

@@ -3,12 +3,12 @@
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
class TestPurchaseOrder(unittest.TestCase): class TestPurchaseOrder(FrappeTestCase):
def test_make_purchase_order(self): def test_make_purchase_order(self):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order

View File

@@ -1,12 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
class TestSupplierScorecard(unittest.TestCase): class TestSupplierScorecard(FrappeTestCase):
def test_create_scorecard(self): def test_create_scorecard(self):
doc = make_supplier_scorecard().insert() doc = make_supplier_scorecard().insert()
@@ -49,7 +49,7 @@ valid_scorecard = [
"min_grade":0.0,"name":"Very Poor", "min_grade":0.0,"name":"Very Poor",
"prevent_rfqs":1, "prevent_rfqs":1,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":30.0, "max_grade":30.0,
"prevent_pos":1, "prevent_pos":1,
"warn_pos":0, "warn_pos":0,
@@ -65,7 +65,7 @@ valid_scorecard = [
"name":"Poor", "name":"Poor",
"prevent_rfqs":1, "prevent_rfqs":1,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":50.0, "max_grade":50.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,
@@ -81,7 +81,7 @@ valid_scorecard = [
"name":"Average", "name":"Average",
"prevent_rfqs":0, "prevent_rfqs":0,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":80.0, "max_grade":80.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,
@@ -97,7 +97,7 @@ valid_scorecard = [
"name":"Excellent", "name":"Excellent",
"prevent_rfqs":0, "prevent_rfqs":0,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":100.0, "max_grade":100.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,

View File

@@ -1,12 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
class TestSupplierScorecardCriteria(unittest.TestCase): class TestSupplierScorecardCriteria(FrappeTestCase):
def test_variables_exist(self): def test_variables_exist(self):
delete_test_scorecards() delete_test_scorecards()
for d in test_good_criteria: for d in test_good_criteria:

View File

@@ -1,16 +1,16 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import ( from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import (
VariablePathNotFound, VariablePathNotFound,
) )
class TestSupplierScorecardVariable(unittest.TestCase): class TestSupplierScorecardVariable(FrappeTestCase):
def test_variable_exist(self): def test_variable_exist(self):
for d in test_existing_variables: for d in test_existing_variables:
my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name")) my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name"))

View File

@@ -2,10 +2,10 @@
# For license information, please see license.txt # For license information, please see license.txt
import unittest
from datetime import datetime from datetime import datetime
import frappe 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.purchase_order import make_purchase_receipt
from erpnext.buying.report.procurement_tracker.procurement_tracker import execute 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 from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestProcurementTracker(unittest.TestCase): class TestProcurementTracker(FrappeTestCase):
def test_result_for_procurement_tracker(self): def test_result_for_procurement_tracker(self):
filters = { filters = {
'company': '_Test Procurement Company', 'company': '_Test Procurement Company',

View File

@@ -3,9 +3,9 @@
# Compiled at: 2019-05-06 09:51:46 # Compiled at: 2019-05-06 09:51:46
# Decompiled by https://python-decompiler.com # Decompiled by https://python-decompiler.com
import unittest
import frappe 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.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -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 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): def test_pending_and_received_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes') po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')

View File

@@ -4,9 +4,9 @@
# Decompiled by https://python-decompiler.com # Decompiled by https://python-decompiler.com
import json import json
import unittest
import frappe 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.purchase_order import make_rm_stock_entry
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -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 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): 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") po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")

View File

@@ -1567,13 +1567,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None tax.rate = None
def validate_account_head(tax, doc): def validate_account_head(idx, account, company):
company = frappe.get_cached_value('Account', account_company = frappe.get_cached_value('Account', account, 'company')
tax.account_head, 'company')
if company != doc.company: if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') 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): def validate_cost_center(tax, doc):

View File

@@ -507,13 +507,41 @@ class StockController(AccountsController):
"voucher_no": self.name, "voucher_no": self.name,
"company": self.company "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")) item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
if item_based_reposting: if item_based_reposting:
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
else: else:
create_repost_item_valuation_entry(args) 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() @frappe.whitelist()
def make_quality_inspections(doctype, docname, items): def make_quality_inspections(doctype, docname, items):

View File

@@ -363,8 +363,6 @@ class Subcontracting():
return return
for row in self.get(self.raw_material_table): 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) key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key): if not self.__transferred_items or not self.__transferred_items.get(key):
return return
@@ -372,12 +370,6 @@ class Subcontracting():
self.__validate_batch_no(row, key) self.__validate_batch_no(row, key)
self.__validate_serial_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): 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'): 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) link = get_link_to_form('Purchase Order', row.purchase_order)

View File

@@ -3,7 +3,7 @@
"allow_events_in_timeline": 0, "allow_events_in_timeline": 0,
"allow_guest_to_view": 0, "allow_guest_to_view": 0,
"allow_import": 0, "allow_import": 0,
"allow_rename": 0, "allow_rename": 1,
"autoname": "field:lost_reason", "autoname": "field:lost_reason",
"beta": 0, "beta": 0,
"creation": "2018-12-28 14:48:51.044975", "creation": "2018-12-28 14:48:51.044975",
@@ -57,7 +57,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-12-28 14:49:43.336437", "modified": "2022-02-16 10:49:43.336437",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Opportunity Lost Reason", "name": "Opportunity Lost Reason",
@@ -150,4 +150,4 @@
"track_changes": 0, "track_changes": 0,
"track_seen": 0, "track_seen": 0,
"track_views": 0 "track_views": 0
} }

View File

@@ -265,7 +265,7 @@ class ProductQuery:
customer = get_customer(silent=True) customer = get_customer(silent=True)
if customer: if customer:
quotation = frappe.get_all("Quotation", fields=["name"], filters= 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) order_by="modified desc", limit_page_length=1)
if quotation: if quotation:
items = frappe.get_all( items = frappe.get_all(
@@ -299,4 +299,4 @@ class ProductQuery:
# slice results manually # slice results manually
result[:self.page_length] result[:self.page_length]
return result return result

View File

@@ -311,7 +311,7 @@ def _get_cart_quotation(party=None):
party = get_party() party = get_party()
quotation = frappe.get_all("Quotation", fields=["name"], filters= 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) order_by="modified desc", limit_page_length=1)
if quotation: if quotation:

View File

@@ -56,13 +56,19 @@ class TestShoppingCart(unittest.TestCase):
return quotation return quotation
def test_get_cart_customer(self): 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 self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer")
quotation = _get_cart_quotation() validate_quotation()
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(quotation.party_name, "_Test Customer") self.login_as_customer()
self.assertEqual(quotation.contact_email, frappe.session.user) quotation = validate_quotation()
return quotation return quotation
@@ -253,10 +259,9 @@ class TestShoppingCart(unittest.TestCase):
self.create_user_if_not_exists("test_cart_user@example.com") self.create_user_if_not_exists("test_cart_user@example.com")
frappe.set_user("test_cart_user@example.com") frappe.set_user("test_cart_user@example.com")
def login_as_customer(self): def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"):
self.create_user_if_not_exists("test_contact_customer@example.com", self.create_user_if_not_exists(email, name)
"_Test Contact For _Test Customer") frappe.set_user(email)
frappe.set_user("test_contact_customer@example.com")
def clear_existing_quotations(self): def clear_existing_quotations(self):
quotations = frappe.get_all("Quotation", filters={ quotations = frappe.get_all("Quotation", filters={

View File

@@ -12,7 +12,7 @@ from six.moves.urllib.parse import urlencode
class GoCardlessSettings(Document): class GoCardlessSettings(Document):
supported_currencies = ["EUR", "DKK", "GBP", "SEK"] supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
def validate(self): def validate(self):
self.initialize_client() self.initialize_client()
@@ -79,7 +79,7 @@ class GoCardlessSettings(Document):
def validate_transaction_currency(self, currency): def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies: 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): def get_payment_url(self, **kwargs):
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs))) return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))

View File

@@ -8,10 +8,6 @@ from frappe.utils import cint, flt
from erpnext import get_default_company, get_region 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", 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", "FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"] "SE", "SI", "SK", "US"]
@@ -35,12 +31,14 @@ def get_client():
if api_key and api_url: if api_key and api_url:
client = taxjar.Client(api_key=api_key, api_url=api_url) client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', { client.set_api_config('headers', {
'x-api-version': '2020-08-07' 'x-api-version': '2022-01-24'
}) })
return client return client
def create_transaction(doc, method): def create_transaction(doc, method):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
"""Create an order transaction in TaxJar""" """Create an order transaction in TaxJar"""
if not TAXJAR_CREATE_TRANSACTIONS: if not TAXJAR_CREATE_TRANSACTIONS:
@@ -51,6 +49,7 @@ def create_transaction(doc, method):
if not client: if not client:
return 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]) sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
if not sales_tax: if not sales_tax:
@@ -79,6 +78,7 @@ def create_transaction(doc, method):
def delete_transaction(doc, method): def delete_transaction(doc, method):
"""Delete an existing TaxJar order transaction""" """Delete an existing TaxJar order transaction"""
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
if not TAXJAR_CREATE_TRANSACTIONS: if not TAXJAR_CREATE_TRANSACTIONS:
return return
@@ -92,6 +92,8 @@ def delete_transaction(doc, method):
def get_tax_data(doc): 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_address = get_company_address_details(doc)
from_shipping_state = from_address.get("state") from_shipping_state = from_address.get("state")
from_country_code = frappe.db.get_value("Country", from_address.country, "code") 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') to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = { tax_dict = {
'from_country': from_country_code, "from_country": from_country_code,
'from_zip': from_address.pincode, "from_zip": from_address.pincode,
'from_state': from_shipping_state, "from_state": from_shipping_state,
'from_city': from_address.city, "from_city": from_address.city,
'from_street': from_address.address_line1, "from_street": from_address.address_line1,
'to_country': to_country_code, "to_country": to_country_code,
'to_zip': to_address.pincode, "to_zip": to_address.pincode,
'to_city': to_address.city, "to_city": to_address.city,
'to_street': to_address.address_line1, "to_street": to_address.address_line1,
'to_state': to_shipping_state, "to_state": to_shipping_state,
'shipping': shipping, "shipping": shipping,
'amount': doc.net_total, "amount": doc.net_total,
'plugin': 'erpnext', "plugin": "erpnext",
'line_items': line_items "line_items": line_items
} }
return tax_dict return tax_dict
@@ -156,6 +158,9 @@ def get_line_item_dict(item, docstatus):
return tax_dict return tax_dict
def set_sales_tax(doc, method): 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: if not TAXJAR_CALCULATE_TAX:
return return
@@ -206,6 +211,7 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals") doc.run_method("calculate_taxes_and_totals")
def check_for_nexus(doc, tax_dict): 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"]}): if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
for item in doc.get("items"): for item in doc.get("items"):
item.tax_collectable = flt(0) item.tax_collectable = flt(0)
@@ -218,6 +224,8 @@ def check_for_nexus(doc, tax_dict):
def check_sales_tax_exemption(doc): def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero # 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 \ 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") \ or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax") and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")

View File

@@ -62,7 +62,7 @@ class JobCard(Document):
if self.get('time_logs'): if self.get('time_logs'):
for d in 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)) frappe.throw(_("Row {0}: From time must be less than to time").format(d.idx))
data = self.get_overlap_for(d) data = self.get_overlap_for(d)

View File

@@ -49,7 +49,7 @@ frappe.ui.form.on('Production Plan', {
if (d.item_code) { if (d.item_code) {
return { return {
query: "erpnext.controllers.queries.bom", 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")); } else frappe.msgprint(__("Please enter Item first"));
} }

View File

@@ -320,7 +320,7 @@ class ProductionPlan(Document):
if self.total_produced_qty > 0: if self.total_produced_qty > 0:
self.status = "In Process" self.status = "In Process"
if self.check_have_work_orders_completed(): if self.all_items_completed():
self.status = "Completed" self.status = "Completed"
if self.status != 'Completed': if self.status != 'Completed':
@@ -592,14 +592,24 @@ class ProductionPlan(Document):
self.append("sub_assembly_items", data) self.append("sub_assembly_items", data)
def check_have_work_orders_completed(self): def all_items_completed(self):
wo_status = frappe.db.get_list( 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", "Work Order",
filters={"production_plan": self.name}, filters={
"production_plan": self.name,
"status": ("not in", ["Closed", "Stopped"]),
"docstatus": ("<", 2),
},
fields="status", 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() @frappe.whitelist()
def download_raw_materials(doc, warehouses=None): 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): def set_default_warehouses(row, default_warehouses):
for field in ['wip_warehouse', 'fg_warehouse']: for field in ['wip_warehouse', 'fg_warehouse']:
if not row.get(field): if not row.get(field):
row[field] = default_warehouses.get(field) row[field] = default_warehouses.get(field)

View File

@@ -9,6 +9,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_sales_orders, get_sales_orders,
get_warehouse_list, 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.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.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -409,9 +410,6 @@ class TestProductionPlan(ERPNextTestCase):
boms = { boms = {
"Assembly": { "Assembly": {
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},}, "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
"SubAssembly2": {"ChildPart3": {}},
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
"ChildPart5": {},
"ChildPart6": {}, "ChildPart6": {},
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}}, "SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
}, },
@@ -469,26 +467,29 @@ class TestProductionPlan(ERPNextTestCase):
bom = make_bom(item=item, raw_materials=raw_materials) bom = make_bom(item=item, raw_materials=raw_materials)
# Create Production Plan # 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 # All the created Work Orders
wo_list = [] wo_list = []
# Create and Submit 1st Work Order for 5 qty # Create and Submit 1st Work Order for 3 qty
create_work_order(item, pln, 5) 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() pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 5) self.assertEqual(pln.po_items[0].ordered_qty, 5)
# Create and Submit 2nd Work Order for 3 qty # Overproduction
create_work_order(item, pln, 3) self.assertRaises(OverProductionError, create_work_order, item=item, pln=pln, qty=2)
pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 8)
# Cancel 1st Work Order # Cancel 1st Work Order
wo1 = frappe.get_doc("Work Order", wo_list[0]) wo1 = frappe.get_doc("Work Order", wo_list[0])
wo1.cancel() wo1.cancel()
pln.reload() pln.reload()
self.assertEqual(pln.po_items[0].ordered_qty, 3) self.assertEqual(pln.po_items[0].ordered_qty, 2)
# Cancel 2nd Work Order # Cancel 2nd Work Order
wo2 = frappe.get_doc("Work Order", wo_list[1]) wo2 = frappe.get_doc("Work Order", wo_list[1])
@@ -591,6 +592,20 @@ class TestProductionPlan(ERPNextTestCase):
pln.reload() pln.reload()
self.assertEqual(pln.po_items[0].pending_qty, 1) 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): def create_production_plan(**args):
""" """
sales_order (obj): Sales Order Doc Object sales_order (obj): Sales Order Doc Object

View File

@@ -632,6 +632,21 @@ class WorkOrder(Document):
if not self.qty > 0: if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 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): def validate_transfer_against(self):
if not self.docstatus == 1: if not self.docstatus == 1:
# let user configure operations until they're ready to submit # 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] res = res[0]
if skip_bom_info: return res if skip_bom_info: return res
filters = {"item": item, "is_default": 1} filters = {"item": item, "is_default": 1, "docstatus": 1}
if project: if project:
filters = {"item": item, "project": project} filters = {"item": item, "project": project}

View File

@@ -1265,7 +1265,7 @@ class SalarySlip(TransactionBase):
for i, earning in enumerate(self.earnings): for i, earning in enumerate(self.earnings):
if earning.salary_component == salary_component: if earning.salary_component == salary_component:
self.earnings[i].amount = wages_amount 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) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
def compute_year_to_date(self): def compute_year_to_date(self):

View File

@@ -6,6 +6,7 @@ import random
import unittest import unittest
import frappe import frappe
from frappe.model.document import Document
from frappe.utils import ( from frappe.utils import (
add_days, add_days,
add_months, 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): def make_salary_component(salary_components, test_tax, company_list=None):
for salary_component in salary_components: for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component["salary_component"]): if frappe.db.exists('Salary Component', salary_component["salary_component"]):
if test_tax: continue
if salary_component["type"] == "Earning":
salary_component["is_tax_applicable"] = 1 if test_tax:
elif salary_component["salary_component"] == "TDS": if salary_component["type"] == "Earning":
salary_component["variable_based_on_taxable_salary"] = 1 salary_component["is_tax_applicable"] = 1
salary_component["amount_based_on_formula"] = 0 elif salary_component["salary_component"] == "TDS":
salary_component["amount"] = 0 salary_component["variable_based_on_taxable_salary"] = 1
salary_component["formula"] = "" salary_component["amount_based_on_formula"] = 0
salary_component["condition"] = "" salary_component["amount"] = 0
salary_component["doctype"] = "Salary Component" salary_component["formula"] = ""
salary_component["salary_component_abbr"] = salary_component["abbr"] salary_component["condition"] = ""
frappe.get_doc(salary_component).insert()
get_salary_component_account(salary_component["salary_component"], company_list) 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): def get_salary_component_account(sal_comp, company_list=None):
company = erpnext.get_default_company() 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: if company_list and company not in company_list:
company_list.append(company) 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"): if not sal_comp.get("accounts"):
for d in company_list: for d in company_list:
company_abbr = frappe.get_cached_value('Company', d, 'abbr') company_abbr = frappe.get_cached_value('Company', d, 'abbr')

View File

@@ -76,9 +76,6 @@ class Task(NestedSet):
if flt(self.progress or 0) > 100: if flt(self.progress or 0) > 100:
frappe.throw(_("Progress % for a task cannot be more than 100.")) frappe.throw(_("Progress % for a task cannot be more than 100."))
if flt(self.progress) == 100:
self.status = 'Completed'
if self.status == 'Completed': if self.status == 'Completed':
self.progress = 100 self.progress = 100

View File

@@ -151,6 +151,35 @@ class TestTimesheet(unittest.TestCase):
settings.ignore_employee_time_overlap = initial_setting settings.ignore_employee_time_overlap = initial_setting
settings.save() 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): def test_to_time(self):
emp = make_employee("test_employee_6@salary.com") emp = make_employee("test_employee_6@salary.com")
from_time = now_datetime() from_time = now_datetime()

View File

@@ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", {
currency: function(frm) { currency: function(frm) {
let base_currency = frappe.defaults.get_global_default('currency'); 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({ frappe.call({
method: "erpnext.setup.utils.get_exchange_rate", method: "erpnext.setup.utils.get_exchange_rate",
args: { args: {

View File

@@ -7,7 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document 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.controllers.queries import get_match_cond
from erpnext.hr.utils import validate_active_employee from erpnext.hr.utils import validate_active_employee
@@ -145,7 +145,7 @@ class Timesheet(Document):
if not (data.from_time and data.hours): if not (data.from_time and data.hours):
return 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: if data.to_time != _to_time:
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) .format(args.idx, self.name, existing.name), OverlapError)
def get_overlap_for(self, fieldname, args, value): def get_overlap_for(self, fieldname, args, value):
cond = "ts.`{0}`".format(fieldname) timesheet = frappe.qb.DocType("Timesheet")
if fieldname == 'workstation': timelog = frappe.qb.DocType("Timesheet Detail")
cond = "tsd.`{0}`".format(fieldname)
existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from from_time = get_datetime(args.from_time)
`tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and to_time = get_datetime(args.to_time)
(
(%(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
if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ existing = (
args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or frappe.qb.from_(timesheet)
(args.to_time > time_log.from_time and args.to_time < time_log.to_time) or .join(timelog)
(args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)): .on(timelog.parent == timesheet.name)
return self .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 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): def update_cost(self):
for data in self.time_logs: for data in self.time_logs:
if data.activity_type or data.is_billable: if data.activity_type or data.is_billable:

View File

@@ -14,12 +14,6 @@
"to_time", "to_time",
"hours", "hours",
"completed", "completed",
"section_break_7",
"completed_qty",
"workstation",
"column_break_12",
"operation",
"operation_id",
"project_details", "project_details",
"project", "project",
"project_name", "project_name",
@@ -83,43 +77,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Completed" "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", "fieldname": "project_details",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -267,7 +224,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-05-18 12:19:33.205940", "modified": "2022-02-17 16:53:34.878798",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Timesheet Detail", "name": "Timesheet Detail",
@@ -275,5 +232,6 @@
"permissions": [], "permissions": [],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@@ -525,6 +525,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
item.weight_per_unit = 0; item.weight_per_unit = 0;
item.weight_uom = ''; item.weight_uom = '';
item.conversion_factor = 0;
if(['Sales Invoice'].includes(this.frm.doc.doctype)) { if(['Sales Invoice'].includes(this.frm.doc.doctype)) {
update_stock = cint(me.frm.doc.update_stock); update_stock = cint(me.frm.doc.update_stock);

View File

@@ -304,12 +304,13 @@ erpnext.HierarchyChart = class {
} }
get_child_nodes(node_id) { get_child_nodes(node_id) {
let me = this;
return new Promise(resolve => { return new Promise(resolve => {
frappe.call({ frappe.call({
method: this.method, method: me.method,
args: { args: {
parent: node_id, parent: node_id,
company: this.company company: me.company
} }
}).then(r => resolve(r.message)); }).then(r => resolve(r.message));
}); });
@@ -350,12 +351,13 @@ erpnext.HierarchyChart = class {
} }
get_all_nodes() { get_all_nodes() {
let me = this;
return new Promise(resolve => { return new Promise(resolve => {
frappe.call({ frappe.call({
method: 'erpnext.utilities.hierarchy_chart.get_all_nodes', method: 'erpnext.utilities.hierarchy_chart.get_all_nodes',
args: { args: {
method: this.method, method: me.method,
company: this.company company: me.company
}, },
callback: (r) => { callback: (r) => {
resolve(r.message); resolve(r.message);
@@ -427,8 +429,8 @@ erpnext.HierarchyChart = class {
add_connector(parent_id, child_id) { add_connector(parent_id, child_id) {
// using pure javascript for better performance // using pure javascript for better performance
const parent_node = document.querySelector(`#${parent_id}`); const parent_node = document.getElementById(`${parent_id}`);
const child_node = document.querySelector(`#${child_id}`); const child_node = document.getElementById(`${child_id}`);
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');

View File

@@ -235,7 +235,7 @@ erpnext.HierarchyChartMobile = class {
let me = this; let me = this;
return new Promise(resolve => { return new Promise(resolve => {
frappe.call({ frappe.call({
method: this.method, method: me.method,
args: { args: {
parent: node_id, parent: node_id,
company: me.company, company: me.company,
@@ -286,8 +286,8 @@ erpnext.HierarchyChartMobile = class {
} }
add_connector(parent_id, child_id) { add_connector(parent_id, child_id) {
const parent_node = document.querySelector(`#${parent_id}`); const parent_node = document.getElementById(`${parent_id}`);
const child_node = document.querySelector(`#${child_id}`); const child_node = document.getElementById(`${child_id}`);
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
@@ -518,7 +518,8 @@ erpnext.HierarchyChartMobile = class {
level.nextAll('li').remove(); level.nextAll('li').remove();
let node_object = this.nodes[node.id]; 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'); current_node.removeClass('active-child active-path');
node_object.expanded = 0; node_object.expanded = 0;

View File

@@ -17,7 +17,7 @@ frappe.query_reports["GSTR-1"] = {
"fieldtype": "Link", "fieldtype": "Link",
"options": "Address", "options": "Address",
"get_query": function () { "get_query": function () {
var company = frappe.query_report.get_filter_value('company'); let company = frappe.query_report.get_filter_value('company');
if (company) { if (company) {
return { return {
"query": 'frappe.contacts.doctype.address.address.address_query', "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", "fieldname": "from_date",
"label": __("From Date"), "label": __("From Date"),
@@ -60,10 +65,21 @@ frappe.query_reports["GSTR-1"] = {
} }
], ],
onload: function (report) { 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 () { report.page.add_inner_button(__("Download as JSON"), function () {
var filters = report.get_values();
frappe.call({ frappe.call({
method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', method: 'erpnext.regional.report.gstr_1.gstr_1.get_json',
args: { args: {

View File

@@ -254,7 +254,8 @@ class Gstr1Report(object):
for opts in (("company", " and company=%(company)s"), for opts in (("company", " and company=%(company)s"),
("from_date", " and posting_date>=%(from_date)s"), ("from_date", " and posting_date>=%(from_date)s"),
("to_date", " and posting_date<=%(to_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]): if self.filters.get(opts[0]):
conditions += opts[1] conditions += opts[1]
@@ -1193,3 +1194,23 @@ def is_inter_state(invoice_detail):
return True return True
else: else:
return False 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

View File

@@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
var me = this; var me = this;
var dialog = new frappe.ui.Dialog({ var dialog = new frappe.ui.Dialog({
title: __("Select Items"), title: __("Select Items"),
size: "large",
fields: [ fields: [
{ {
"fieldtype": "Check", "fieldtype": "Check",
@@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
} else { } else {
let po_items = []; let po_items = [];
me.frm.doc.items.forEach(d => { 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) { if (pending_qty > 0) {
po_items.push({ po_items.push({
"doctype": "Sales Order Item", "doctype": "Sales Order Item",
@@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
dialog.show(); 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(){ hold_sales_order: function(){
var me = this; var me = this;
var d = new frappe.ui.Dialog({ var d = new frappe.ui.Dialog({

View File

@@ -923,6 +923,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project 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")}) # po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
doc = get_mapped_doc("Sales Order", source_name, { doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": { "Sales Order": {
@@ -966,6 +969,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"Packed Item": { "Packed Item": {
"doctype": "Purchase Order Item", "doctype": "Purchase Order Item",
"field_map": [ "field_map": [
["name", "sales_order_packed_item"],
["parent", "sales_order"], ["parent", "sales_order"],
["uom", "uom"], ["uom", "uom"],
["conversion_factor", "conversion_factor"], ["conversion_factor", "conversion_factor"],
@@ -980,6 +984,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"supplier", "supplier",
"pricing_rules" "pricing_rules"
], ],
"postprocess": update_item_for_packed_item,
"condition": lambda doc: doc.parent_item in items_to_map "condition": lambda doc: doc.parent_item in items_to_map
} }
}, target_doc, set_missing_values) }, target_doc, set_missing_values)

View File

@@ -921,6 +921,74 @@ class TestSalesOrder(ERPNextTestCase):
self.assertEqual(purchase_orders[0].supplier, '_Test Supplier') self.assertEqual(purchase_orders[0].supplier, '_Test Supplier')
self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1') 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): def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"]) fields=["reserved_qty"])

View File

@@ -791,6 +791,7 @@
}, },
{ {
"default": "0", "default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission", "fieldname": "grant_commission",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Grant Commission", "label": "Grant Commission",
@@ -800,7 +801,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-02-21 13:55:08.883104", "modified": "2022-02-24 14:41:57.325799",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Item", "name": "Sales Order Item",

View File

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

View File

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

View File

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

View File

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

View File

@@ -757,6 +757,7 @@
}, },
{ {
"default": "0", "default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission", "fieldname": "grant_commission",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Grant Commission", "label": "Grant Commission",
@@ -767,12 +768,14 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-10-06 12:12:44.018872", "modified": "2022-02-24 14:42:20.211085",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
} "states": []
}

View File

@@ -594,7 +594,7 @@ $.extend(erpnext.item, {
const increment = r.message.increment; const increment = r.message.increment;
let values = []; let values = [];
for(var i = from; i <= to; i += increment) { for(var i = from; i <= to; i = flt(i + increment, 6)) {
values.push(i); values.push(i);
} }
attr_val_fields[d.attribute] = values; attr_val_fields[d.attribute] = values;

View File

@@ -399,6 +399,7 @@ class Item(Document):
if merge: if merge:
self.validate_properties_before_merge(new_name) 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) self.validate_duplicate_website_item_before_merge(old_name, new_name)
def after_rename(self, old_name, new_name, merge): 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]) msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError) 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): 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. 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] 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) 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) frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
def set_last_purchase_rate(self, new_name): def set_last_purchase_rate(self, new_name):

View File

@@ -14,6 +14,7 @@ from erpnext.controllers.item_variant import (
get_variant, get_variant,
) )
from erpnext.stock.doctype.item.item import ( from erpnext.stock.doctype.item.item import (
DataValidationError,
InvalidBarcode, InvalidBarcode,
StockExistsForTemplate, StockExistsForTemplate,
get_item_attribute, get_item_attribute,
@@ -387,6 +388,26 @@ class TestItem(ERPNextTestCase):
self.assertTrue(frappe.db.get_value("Bin", self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) {"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): def test_uom_conversion_factor(self):
if frappe.db.exists('Item', 'Test Item UOM'): if frappe.db.exists('Item', 'Test Item UOM'):
frappe.delete_doc('Item', 'Test Item UOM') frappe.delete_doc('Item', 'Test Item UOM')

View File

@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.utils import update_gl_entries_after 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.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 ( from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries, get_gl_entries,
make_purchase_receipt, 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.purchase_rate - serial_no_rate, 5.0)
self.assertEqual(serial_no.warehouse, "Stores - TCP1") 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): 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) pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)

View File

@@ -57,14 +57,13 @@ class MaterialRequest(BuyingController):
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty): 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)) 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): def validate(self):
super(MaterialRequest, self).validate() super(MaterialRequest, self).validate()
self.validate_schedule_date() self.validate_schedule_date()
self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order') self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
self.validate_material_request_type()
if not self.status: if not self.status:
self.status = "Draft" 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_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_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): def set_title(self):
'''Set title as comma separated list of items''' '''Set title as comma separated list of items'''
if not self.title: if not self.title:

View File

@@ -26,6 +26,7 @@
"section_break_13", "section_break_13",
"actual_qty", "actual_qty",
"projected_qty", "projected_qty",
"ordered_qty",
"column_break_16", "column_break_16",
"incoming_rate", "incoming_rate",
"page_break", "page_break",
@@ -224,13 +225,21 @@
"label": "Rate", "label": "Rate",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "ordered_qty",
"fieldtype": "Float",
"label": "Ordered Qty",
"no_copy": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-01-28 16:03:30.780111", "modified": "2022-02-22 12:57:45.325488",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Packed Item", "name": "Packed Item",

View File

@@ -162,6 +162,15 @@ class TestPurchaseReceipt(ERPNextTestCase):
qty=abs(existing_bin_qty) 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() pr = make_purchase_receipt()
stock_value_difference = frappe.db.get_value( stock_value_difference = frappe.db.get_value(

View File

@@ -9,8 +9,7 @@ from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, floor, flt, nowdate from frappe.utils import cint, cstr, floor, flt, nowdate
from six import string_types
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance 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 purpose: Purpose of Stock Entry
sync (optional): Sync with client side only for client side calls 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 = json.loads(items)
items_not_accomodated, updated_table = [], [] items_not_accomodated, updated_table = [], []
@@ -143,11 +142,44 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if items_not_accomodated: if items_not_accomodated:
show_unassigned_items_message(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 if sync and json.loads(sync): # sync with client side
return items 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): def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
"""Returns an ordered list of putaway rules to apply on an item.""" """Returns an ordered list of putaway rules to apply on an item."""
filters = { filters = {

View File

@@ -35,6 +35,18 @@ class TestPutawayRule(ERPNextTestCase):
new_uom.uom_name = "Bag" new_uom.uom_name = "Bag"
new_uom.save() 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): def test_putaway_rules_priority(self):
"""Test if rule is applied by priority, irrespective of free space.""" """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, 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].qty, 100)
self.assertEqual(pr.items[1].warehouse, self.warehouse_2) self.assertEqual(pr.items[1].warehouse, self.warehouse_2)
self.assertUnchangedItemsOnResave(pr)
pr.delete() pr.delete()
rule_1.delete() rule_1.delete()
rule_2.delete() rule_2.delete()
@@ -162,6 +176,8 @@ class TestPutawayRule(ERPNextTestCase):
# leftover space was for 500 kg (0.5 Bag) # leftover space was for 500 kg (0.5 Bag)
# Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
self.assertUnchangedItemsOnResave(pr)
pr.delete() pr.delete()
rule_1.delete() rule_1.delete()
rule_2.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].warehouse, self.warehouse_1)
self.assertEqual(pr.items[1].putaway_rule, rule_1.name) self.assertEqual(pr.items[1].putaway_rule, rule_1.name)
self.assertUnchangedItemsOnResave(pr)
pr.delete() pr.delete()
rule_1.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.qty, 100) # unassigned 100 out of 200 Kg
self.assertEqual(stock_entry_item.putaway_rule, rule_2.name) self.assertEqual(stock_entry_item.putaway_rule, rule_2.name)
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete() stock_entry.delete()
rule_1.delete() rule_1.delete()
rule_2.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].qty, 200)
self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name) self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name)
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete() stock_entry.delete()
rule_1.delete() rule_1.delete()
rule_2.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].serial_no, "\n".join(serial_nos[3:]))
self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1") self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete() stock_entry.delete()
pr.cancel() pr.cancel()
rule_1.delete() rule_1.delete()
@@ -366,6 +390,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry_item.qty, 100) self.assertEqual(stock_entry_item.qty, 100)
self.assertEqual(stock_entry_item.putaway_rule, rule_1.name) self.assertEqual(stock_entry_item.putaway_rule, rule_1.name)
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete() stock_entry.delete()
rule_1.delete() rule_1.delete()
rule_2.delete() rule_2.delete()

View File

@@ -627,6 +627,12 @@ frappe.ui.form.on('Stock Entry Detail', {
frm.events.set_serial_no(frm, cdt, cdn, () => { frm.events.set_serial_no(frm, cdt, cdn, () => {
frm.events.get_warehouse_details(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) { t_warehouse: function(frm, cdt, cdn) {

View File

@@ -45,6 +45,7 @@ def get_sle(**args):
class TestStockEntry(ERPNextTestCase): class TestStockEntry(ERPNextTestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0") frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
@@ -566,6 +567,7 @@ class TestStockEntry(ERPNextTestCase):
st1.set_stock_entry_type() st1.set_stock_entry_type()
st1.insert() st1.insert()
st1.submit() st1.submit()
st1.cancel()
frappe.set_user("Administrator") frappe.set_user("Administrator")
remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com") 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", bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
"is_default": 1, "docstatus": 1}) "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 = frappe.new_doc("Work Order")
work_order.update({ work_order.update({
"company": "_Test Company", "company": "_Test Company",
@@ -1101,13 +1105,10 @@ class TestStockEntry(ERPNextTestCase):
# Check if FG cost is calculated based on RM total cost # Check if FG cost is calculated based on RM total cost
# RM total cost = 200, FG rate = 200/4(FG qty) = 50 # 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.value_difference, 0.0)
self.assertEqual(se.total_incoming_value, se.total_outgoing_value) self.assertEqual(se.total_incoming_value, se.total_outgoing_value)
# teardown
se.delete()
def make_serialized_item(**args): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)
se = frappe.copy_doc(test_records[0]) se = frappe.copy_doc(test_records[0])

View File

@@ -1,7 +1,7 @@
{ {
"actions": [], "actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-03-29 18:22:12", "creation": "2022-02-05 00:17:49.860824",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 1, "editable_grid": 1,
@@ -340,13 +340,13 @@
"label": "More Information" "label": "More Information"
}, },
{ {
"allow_on_submit": 1,
"default": "0", "default": "0",
"fieldname": "allow_zero_valuation_rate", "fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Valuation Rate", "label": "Allow Zero Valuation Rate",
"no_copy": 1, "no_copy": 1,
"print_hide": 1 "print_hide": 1,
"read_only_depends_on": "eval:doc.s_warehouse"
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@@ -556,12 +556,14 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-22 16:47:11.268975", "modified": "2022-02-26 00:51:24.963653",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",
"naming_rule": "Random",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import json
import frappe import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today 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 `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) 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): def test_item_cost_reposting(self):
company = "_Test Company" company = "_Test Company"
@@ -349,6 +372,77 @@ class TestStockLedgerEntry(ERPNextTestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
user.remove_roles("Stock Manager") 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): def create_repack_entry(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -21,6 +21,7 @@ SLE_FIELDS = (
"stock_value", "stock_value",
"stock_value_difference", "stock_value_difference",
"valuation_rate", "valuation_rate",
"voucher_detail_no",
) )
@@ -60,10 +61,15 @@ def add_invariant_check_fields(sles):
fifo_qty += qty fifo_qty += qty
fifo_value += qty * rate 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_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: 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_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value sle.fifo_stock_value = fifo_value
@@ -145,9 +151,9 @@ def get_columns():
"label": "Incoming Rate", "label": "Incoming Rate",
}, },
{ {
"fieldname": "outgoing_rate", "fieldname": "consumption_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Outgoing Rate", "label": "Consumption Rate",
}, },
{ {
"fieldname": "qty_after_transaction", "fieldname": "qty_after_transaction",

View File

@@ -23,9 +23,18 @@ class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError): class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass pass
_exceptions = frappe.local('stockledger_exceptions')
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): 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 from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries: if sl_entries:
cancel = sl_entries[0].get("is_cancelled") 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) future_sle_exists(args, sl_entries)
for sle in 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) validate_serial_no(sle)
if cancel: if cancel:
@@ -459,6 +468,8 @@ class update_entries_after(object):
# rounding as per precision # rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.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 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 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: 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) allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate: if not allow_zero_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.wh_data.valuation_rate = self.get_fallback_rate(sle)
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
def get_incoming_value_for_serial_nos(self, sle, serial_nos): def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company # 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: 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) allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate: if not allow_zero_valuation_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, self.wh_data.valuation_rate = self.get_fallback_rate(sle)
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
def get_fifo_values(self, sle): def get_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate) 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 # 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) allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate: if not allow_zero_valuation_rate:
_rate = get_valuation_rate(sle.item_code, sle.warehouse, _rate = self.get_fallback_rate(sle)
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
else: else:
_rate = 0 _rate = 0
@@ -788,6 +793,13 @@ class update_entries_after(object):
else: else:
return 0 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): def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket""" """get previous stock ledger entry before current time-bucket"""
sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False) sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)

View File

@@ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase):
Test Stock and Service Item Search. 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}) item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
make_stock_entry( make_stock_entry(
item_code="Test Search Stock Item", item_code="Test Search Stock Item",

View File

@@ -66,6 +66,20 @@ def create_test_contact_and_address():
contact.add_phone("+91 0000000000", is_primary_phone=True) contact.add_phone("+91 0000000000", is_primary_phone=True)
contact.insert() 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 @contextmanager
def change_settings(doctype, settings_dict): def change_settings(doctype, settings_dict):

View File

@@ -3726,7 +3726,7 @@ Earliest Age,Frühestes Alter,
Edit Details,Details bearbeiten, Edit Details,Details bearbeiten,
Edit Profile,Profil 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, 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, Email Campaigns,E-Mail-Kampagnen,
Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft, Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
Employee Tax and Benefits,Mitarbeitersteuern und -leistungen, 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, Send Emails At,Die E-Mails senden um,
Reminder,Erinnerung, Reminder,Erinnerung,
Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer, Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
email,Email, email,E-Mail,
Parent Department,Elternabteilung, Parent Department,Elternabteilung,
Leave Block List,Urlaubssperrenliste, Leave Block List,Urlaubssperrenliste,
Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.", 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.

View File

@@ -11,7 +11,7 @@
{% if frappe.session.user == 'Guest' %} {% if frappe.session.user == 'Guest' %}
<a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a> <a id="signup" class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
{% elif not has_access %} {% 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 %} {% endif %}
</p> </p>
</div> </div>
@@ -20,34 +20,35 @@
<script type="text/javascript"> <script type="text/javascript">
frappe.ready(() => { frappe.ready(() => {
btn = document.getElementById('enroll'); btn = document.getElementById('enroll');
if (btn) btn.disabled = false;
}) })
function enroll() { function enroll() {
let params = frappe.utils.get_query_params() let params = frappe.utils.get_query_params()
let btn = document.getElementById('enroll'); let btn = document.getElementById('enroll');
btn.disbaled = true;
btn.innerText = __('Enrolling...')
let opts = { let opts = {
method: 'erpnext.education.utils.enroll_in_program', method: 'erpnext.education.utils.enroll_in_program',
args: { args: {
program_name: params.program program_name: params.program
} },
freeze: true,
freeze_message: __('Enrolling...')
} }
frappe.call(opts).then(res => { frappe.call(opts).then(res => {
let success_dialog = new frappe.ui.Dialog({ let success_dialog = new frappe.ui.Dialog({
title: __('Success'), title: __('Success'),
primary_action_label: __('View Program Content'),
primary_action: function() {
window.location.reload();
},
secondary_action: function() { 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(); success_dialog.show();
btn.disbaled = false; success_dialog.set_message(__('You have successfully enrolled for the program '));
}) })
} }
</script> </script>