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

View File

@@ -166,7 +166,7 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0,
"update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
"invoice_number": row.invoice_number,
"disable_rounded_total": 1
})

View File

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

View File

@@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_from_account_currency != company_currency));
if (frm.doc.payment_type == "Pay") {
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_to_account_currency != company_currency));
} else {
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_from_account_currency != company_currency));
}
frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency
@@ -232,7 +238,8 @@ frappe.ui.form.on('Payment Entry', {
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
"base_total_taxes_and_charges"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
@@ -341,6 +348,8 @@ frappe.ui.form.on('Payment Entry', {
}
frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
@@ -374,7 +383,11 @@ frappe.ui.form.on('Payment Entry', {
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
}
},
() => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
frm.doc.paid_from_account_currency, company_currency),
() => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency)
]);
}
}
@@ -478,14 +491,14 @@ frappe.ui.form.on('Payment Entry', {
},
paid_from_account_currency: function(frm) {
if(!frm.doc.paid_from_account_currency) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
@@ -505,8 +518,8 @@ frappe.ui.form.on('Payment Entry', {
},
paid_to_account_currency: function(frm) {
if(!frm.doc.paid_to_account_currency) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency);

View File

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

View File

@@ -946,8 +946,12 @@ class PaymentEntry(AccountsController):
tax.base_total = tax.total * self.source_exchange_rate
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
if self.payment_type == 'Pay':
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
if self.get('taxes'):
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
@@ -1078,7 +1082,7 @@ def get_outstanding_reference_documents(args):
if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
# Get all SO / PO which are not fully billed or aginst which full advance not paid
# Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = []
if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),

View File

@@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def test_multi_currency_payment_entry_with_taxes(self):
payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC',
save=True)
payment_entry.append('taxes', {
'account_head': '_Test Account Service Tax - _TC',
'charge_type': 'Actual',
'tax_amount': 10,
'add_deduct_tax': 'Add',
'description': 'Test'
})
payment_entry.save()
self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
def create_payment_entry(**args):
payment_entry = frappe.new_doc('Payment Entry')
payment_entry.company = args.get('company') or '_Test Company'
payment_entry.payment_type = args.get('payment_type') or 'Pay'
payment_entry.party_type = args.get('party_type') or 'Supplier'
payment_entry.party = args.get('party') or '_Test Supplier'
payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
payment_entry.paid_amount = args.get('paid_amount') or 1000
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_exchange_rate()
payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
payment_entry.reference_no = 'Test001'
payment_entry.reference_date = nowdate()
if args.get('save'):
payment_entry.save()
if args.get('submit'):
payment_entry.submit()
return payment_entry
def create_payment_terms_template():
create_payment_term('Basic Amount Receivable')

View File

@@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True):
'to_date': doc.to_date,
'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None,
'account': doc.account if doc.account else None,
'account': [doc.account] if doc.account else None,
'party_type': 'Customer',
'party': [entry.customer],
'presentation_currency': presentation_currency,

View File

@@ -1615,6 +1615,56 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self):
si = create_sales_invoice(do_not_save=True)
si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
si.append("items", {
"item_code": "_Test Item",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": d[1],
"rate": d[0],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
})
for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": tax_account,
"description": tax_account,
"rate": 6,
"cost_center": "_Test Cost Center - _TC",
"included_in_print_rate": 1
})
si.save()
si.submit()
self.assertEqual(si.net_total, 4007.16)
self.assertEqual(si.grand_total, 4488.02)
self.assertEqual(si.total_taxes_and_charges, 480.86)
self.assertEqual(si.rounding_adjustment, -0.02)
expected_values = dict((d[0], d) for d in [
[si.debit_to, 4488.0, 0.0],
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
["Round Off - _TC", 0.01, 0]
])
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
order by account asc""", si.name, as_dict=1)
debit_credit_diff = 0
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
debit_credit_diff += (gle.debit - gle.credit)
self.assertEqual(debit_credit_diff, 0)
def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
@@ -2366,14 +2416,22 @@ class TestSalesInvoice(unittest.TestCase):
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
si = frappe.copy_doc(test_records[2])
frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1)
frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0)
item = copy.deepcopy(si.get('items')[0])
item.update({
"qty": 1,
"rate": 500,
"grant_commission": 1
})
si.append("items", item)
item = copy.deepcopy(si.get('items')[1])
item.update({
"qty": 1,
"rate": 500,
})
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):

View File

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

View File

@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_account_head(tax, doc)
validate_account_head(tax.idx, tax.account_head, doc.company)
validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc)
@@ -55,5 +55,8 @@ def validate_disabled(doc):
frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc):
if not doc.tax_category:
return
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))

View File

@@ -221,7 +221,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
debit_credit_diff += flt(d.credit)
round_off_account_exists = True
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
gl_map.remove(round_off_gle)
return

View File

@@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc):
from erpnext.controllers.accounts_controller import validate_account_head
companies = []
for account in doc.get("accounts"):
@@ -331,6 +331,9 @@ def validate_party_accounts(doc):
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
# validate if account is mapped for same company
validate_account_head(account.idx, account.account, account.company)
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):

View File

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

View File

@@ -418,11 +418,12 @@ class Asset(AccountsController):
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
.format(row.idx))
.format(row.idx), title=_("Invalid Schedule"))
if not row.depreciation_start_date:
if not self.available_for_use_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
frappe.throw(_("Row {0}: Depreciation Start Date is required")
.format(row.idx), title=_("Invalid Schedule"))
row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset:
@@ -440,8 +441,9 @@ class Asset(AccountsController):
else:
self.number_of_depreciations_booked = 0
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
.format(row.idx), title=_("Invalid Schedule"))
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")

View File

@@ -820,8 +820,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self):
"""Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations."""
"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
# number_of_depreciations_booked > total_number_of_depreciations
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
@@ -836,6 +837,21 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
# number_of_depreciations_booked = total_number_of_depreciations
asset_2 = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = "2019-12-31",
total_number_of_depreciations = 5,
expected_value_after_useful_life = 10000,
depreciation_start_date = "2020-07-01",
opening_accumulated_depreciation = 10000,
number_of_depreciations_booked = 5,
do_not_save = 1
)
self.assertRaises(frappe.ValidationError, asset_2.save)
def test_depreciation_start_date_is_before_purchase_date(self):
asset = create_asset(
item_code = "Macbook Pro",

View File

@@ -68,6 +68,28 @@ frappe.ui.form.on('Asset Repair', {
});
frappe.ui.form.on('Asset Repair Consumed Item', {
item_code: function(frm, cdt, cdn) {
var item = locals[cdt][cdn];
let item_args = {
'item_code': item.item_code,
'warehouse': frm.doc.warehouse,
'qty': item.consumed_quantity,
'serial_no': item.serial_no,
'company': frm.doc.company
};
frappe.call({
method: 'erpnext.stock.utils.get_incoming_rate',
args: {
args: item_args
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
}
});
},
consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);

View File

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

View File

@@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty'
})
self.status_updater.append({
'source_dt': 'Purchase Order Item',
'target_dt': 'Packed Item',
'target_field': 'ordered_qty',
'target_parent_dt': 'Sales Order',
'target_parent_field': '',
'join_field': 'sales_order_packed_item',
'target_ref_field': 'qty',
'source_field': 'stock_qty'
})
def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship"""

View File

@@ -3,9 +3,9 @@
import json
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -27,7 +27,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestPurchaseOrder(unittest.TestCase):
class TestPurchaseOrder(FrappeTestCase):
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,9 +3,9 @@
# Compiled at: 2019-05-06 09:51:46
# Decompiled by https://python-decompiler.com
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -15,7 +15,7 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestSubcontractedItemToBeReceived(unittest.TestCase):
class TestSubcontractedItemToBeReceived(FrappeTestCase):
def test_pending_and_received_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')

View File

@@ -4,9 +4,9 @@
# Decompiled by https://python-decompiler.com
import json
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -16,7 +16,7 @@ from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcont
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
class TestSubcontractedItemToBeTransferred(FrappeTestCase):
def test_pending_and_transferred_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")

View File

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

View File

@@ -507,13 +507,41 @@ class StockController(AccountsController):
"voucher_no": self.name,
"company": self.company
})
if future_sle_exists(args):
if future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
if item_based_reposting:
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
else:
create_repost_item_valuation_entry(args)
def repost_required_for_queue(doc: StockController) -> bool:
"""check if stock document contains repeated item-warehouse with queue based valuation.
if queue exists for repeated items then SLEs need to reprocessed in background again.
"""
consuming_sles = frappe.db.get_all("Stock Ledger Entry",
filters={
"voucher_type": doc.doctype,
"voucher_no": doc.name,
"actual_qty": ("<", 0),
"is_cancelled": 0
},
fields=["item_code", "warehouse", "stock_queue"]
)
item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
unique_item_warehouses = set(item_warehouses)
if len(unique_item_warehouses) == len(item_warehouses):
return False
for sle in consuming_sles:
if sle.stock_queue != "[]": # using FIFO/LIFO valuation
return True
return False
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):

View File

@@ -363,8 +363,6 @@ class Subcontracting():
return
for row in self.get(self.raw_material_table):
self.__validate_consumed_qty(row)
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
@@ -372,12 +370,6 @@ class Subcontracting():
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def __validate_consumed_qty(self, row):
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
def __validate_batch_no(self, row, key):
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
link = get_link_to_form('Purchase Order', row.purchase_order)

View File

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

View File

@@ -265,7 +265,7 @@ class ProductQuery:
customer = get_customer(silent=True)
if customer:
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
{"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:
items = frappe.get_all(
@@ -299,4 +299,4 @@ class ProductQuery:
# slice results manually
result[:self.page_length]
return result
return result

View File

@@ -311,7 +311,7 @@ def _get_cart_quotation(party=None):
party = get_party()
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0},
{"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:

View File

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

View File

@@ -12,7 +12,7 @@ from six.moves.urllib.parse import urlencode
class GoCardlessSettings(Document):
supported_currencies = ["EUR", "DKK", "GBP", "SEK"]
supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
def validate(self):
self.initialize_client()
@@ -79,7 +79,7 @@ class GoCardlessSettings(Document):
def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies:
frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency))
frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
def get_payment_url(self, **kwargs):
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))

View File

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

View File

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

View File

@@ -49,7 +49,7 @@ frappe.ui.form.on('Production Plan', {
if (d.item_code) {
return {
query: "erpnext.controllers.queries.bom",
filters:{'item': cstr(d.item_code)}
filters:{'item': cstr(d.item_code), 'docstatus': 1}
}
} else frappe.msgprint(__("Please enter Item first"));
}

View File

@@ -320,7 +320,7 @@ class ProductionPlan(Document):
if self.total_produced_qty > 0:
self.status = "In Process"
if self.check_have_work_orders_completed():
if self.all_items_completed():
self.status = "Completed"
if self.status != 'Completed':
@@ -592,14 +592,24 @@ class ProductionPlan(Document):
self.append("sub_assembly_items", data)
def check_have_work_orders_completed(self):
wo_status = frappe.db.get_list(
def all_items_completed(self):
all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001
for d in self.po_items)
if not all_items_produced:
return False
wo_status = frappe.get_all(
"Work Order",
filters={"production_plan": self.name},
filters={
"production_plan": self.name,
"status": ("not in", ["Closed", "Stopped"]),
"docstatus": ("<", 2),
},
fields="status",
pluck="status"
pluck="status",
)
return all(s == "Completed" for s in wo_status)
all_work_orders_completed = all(s == "Completed" for s in wo_status)
return all_work_orders_completed
@frappe.whitelist()
def download_raw_materials(doc, warehouses=None):
@@ -1047,4 +1057,4 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
def set_default_warehouses(row, default_warehouses):
for field in ['wip_warehouse', 'fg_warehouse']:
if not row.get(field):
row[field] = default_warehouses.get(field)
row[field] = default_warehouses.get(field)

View File

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

View File

@@ -632,6 +632,21 @@ class WorkOrder(Document):
if not self.qty > 0:
frappe.throw(_("Quantity to Manufacture must be greater than 0."))
if self.production_plan and self.production_plan_item:
qty_dict = frappe.db.get_value("Production Plan Item", self.production_plan_item, ["planned_qty", "ordered_qty"], as_dict=1)
allowance_qty =flt(frappe.db.get_single_value("Manufacturing Settings",
"overproduction_percentage_for_work_order"))/100 * qty_dict.get("planned_qty", 0)
max_qty = qty_dict.get("planned_qty", 0) + allowance_qty - qty_dict.get("ordered_qty", 0)
if max_qty < 1:
frappe.throw(_("Cannot produce more item for {0}")
.format(self.production_item), OverProductionError)
elif self.qty > max_qty:
frappe.throw(_("Cannot produce more than {0} items for {1}")
.format(max_qty, self.production_item), OverProductionError)
def validate_transfer_against(self):
if not self.docstatus == 1:
# let user configure operations until they're ready to submit
@@ -835,7 +850,7 @@ def get_item_details(item, project = None, skip_bom_info=False):
res = res[0]
if skip_bom_info: return res
filters = {"item": item, "is_default": 1}
filters = {"item": item, "is_default": 1, "docstatus": 1}
if project:
filters = {"item": item, "project": project}

View File

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

View File

@@ -6,6 +6,7 @@ import random
import unittest
import frappe
from frappe.model.document import Document
from frappe.utils import (
add_days,
add_months,
@@ -692,20 +693,25 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
def make_salary_component(salary_components, test_tax, company_list=None):
for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component["salary_component"]):
if test_tax:
if salary_component["type"] == "Earning":
salary_component["is_tax_applicable"] = 1
elif salary_component["salary_component"] == "TDS":
salary_component["variable_based_on_taxable_salary"] = 1
salary_component["amount_based_on_formula"] = 0
salary_component["amount"] = 0
salary_component["formula"] = ""
salary_component["condition"] = ""
salary_component["doctype"] = "Salary Component"
salary_component["salary_component_abbr"] = salary_component["abbr"]
frappe.get_doc(salary_component).insert()
get_salary_component_account(salary_component["salary_component"], company_list)
if frappe.db.exists('Salary Component', salary_component["salary_component"]):
continue
if test_tax:
if salary_component["type"] == "Earning":
salary_component["is_tax_applicable"] = 1
elif salary_component["salary_component"] == "TDS":
salary_component["variable_based_on_taxable_salary"] = 1
salary_component["amount_based_on_formula"] = 0
salary_component["amount"] = 0
salary_component["formula"] = ""
salary_component["condition"] = ""
salary_component["salary_component_abbr"] = salary_component["abbr"]
doc = frappe.new_doc("Salary Component")
doc.update(salary_component)
doc.insert()
get_salary_component_account(doc, company_list)
def get_salary_component_account(sal_comp, company_list=None):
company = erpnext.get_default_company()
@@ -713,7 +719,9 @@ def get_salary_component_account(sal_comp, company_list=None):
if company_list and company not in company_list:
company_list.append(company)
sal_comp = frappe.get_doc("Salary Component", sal_comp)
if not isinstance(sal_comp, Document):
sal_comp = frappe.get_doc("Salary Component", sal_comp)
if not sal_comp.get("accounts"):
for d in company_list:
company_abbr = frappe.get_cached_value('Company', d, 'abbr')

View File

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

View File

@@ -151,6 +151,35 @@ class TestTimesheet(unittest.TestCase):
settings.ignore_employee_time_overlap = initial_setting
settings.save()
def test_timesheet_not_overlapping_with_continuous_timelogs(self):
emp = make_employee("test_employee_6@salary.com")
update_activity_type("_Test Activity Type")
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = emp
timesheet.append(
'time_logs',
{
"billable": 1,
"activity_type": "_Test Activity Type",
"from_time": now_datetime(),
"to_time": now_datetime() + datetime.timedelta(hours=3),
"company": "_Test Company"
}
)
timesheet.append(
'time_logs',
{
"billable": 1,
"activity_type": "_Test Activity Type",
"from_time": now_datetime() + datetime.timedelta(hours=3),
"to_time": now_datetime() + datetime.timedelta(hours=4),
"company": "_Test Company"
}
)
timesheet.save() # should not throw an error
def test_to_time(self):
emp = make_employee("test_employee_6@salary.com")
from_time = now_datetime()

View File

@@ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", {
currency: function(frm) {
let base_currency = frappe.defaults.get_global_default('currency');
if (base_currency != frm.doc.currency) {
if (frm.doc.currency && (base_currency != frm.doc.currency)) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {

View File

@@ -7,7 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
from frappe.utils import add_to_date, flt, get_datetime, getdate, time_diff_in_hours
from erpnext.controllers.queries import get_match_cond
from erpnext.hr.utils import validate_active_employee
@@ -145,7 +145,7 @@ class Timesheet(Document):
if not (data.from_time and data.hours):
return
_to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
_to_time = get_datetime(add_to_date(data.from_time, hours=data.hours, as_datetime=True))
if data.to_time != _to_time:
data.to_time = _to_time
@@ -171,39 +171,54 @@ class Timesheet(Document):
.format(args.idx, self.name, existing.name), OverlapError)
def get_overlap_for(self, fieldname, args, value):
cond = "ts.`{0}`".format(fieldname)
if fieldname == 'workstation':
cond = "tsd.`{0}`".format(fieldname)
timesheet = frappe.qb.DocType("Timesheet")
timelog = frappe.qb.DocType("Timesheet Detail")
existing = frappe.db.sql("""select ts.name as name, tsd.from_time as from_time, tsd.to_time as to_time from
`tabTimesheet Detail` tsd, `tabTimesheet` ts where {0}=%(val)s and tsd.parent = ts.name and
(
(%(from_time)s > tsd.from_time and %(from_time)s < tsd.to_time) or
(%(to_time)s > tsd.from_time and %(to_time)s < tsd.to_time) or
(%(from_time)s <= tsd.from_time and %(to_time)s >= tsd.to_time))
and tsd.name!=%(name)s
and ts.name!=%(parent)s
and ts.docstatus < 2""".format(cond),
{
"val": value,
"from_time": args.from_time,
"to_time": args.to_time,
"name": args.name or "No Name",
"parent": args.parent or "No Name"
}, as_dict=True)
# check internal overlap
for time_log in self.time_logs:
if not (time_log.from_time and time_log.to_time
and args.from_time and args.to_time): continue
from_time = get_datetime(args.from_time)
to_time = get_datetime(args.to_time)
if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \
args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or
(args.to_time > time_log.from_time and args.to_time < time_log.to_time) or
(args.from_time <= time_log.from_time and args.to_time >= time_log.to_time)):
return self
existing = (
frappe.qb.from_(timesheet)
.join(timelog)
.on(timelog.parent == timesheet.name)
.select(timesheet.name.as_('name'), timelog.from_time.as_('from_time'), timelog.to_time.as_('to_time'))
.where(
(timelog.name != (args.name or "No Name"))
& (timesheet.name != (args.parent or "No Name"))
& (timesheet.docstatus < 2)
& (timesheet[fieldname] == value)
& (
((from_time > timelog.from_time) & (from_time < timelog.to_time))
| ((to_time > timelog.from_time) & (to_time < timelog.to_time))
| ((from_time <= timelog.from_time) & (to_time >= timelog.to_time))
)
)
).run(as_dict=True)
if self.check_internal_overlap(fieldname, args):
return self
return existing[0] if existing else None
def check_internal_overlap(self, fieldname, args):
for time_log in self.time_logs:
if not (time_log.from_time and time_log.to_time
and args.from_time and args.to_time):
continue
from_time = get_datetime(time_log.from_time)
to_time = get_datetime(time_log.to_time)
args_from_time = get_datetime(args.from_time)
args_to_time = get_datetime(args.to_time)
if (args.get(fieldname) == time_log.get(fieldname)) and (args.idx != time_log.idx) and (
(args_from_time > from_time and args_from_time < to_time)
or (args_to_time > from_time and args_to_time < to_time)
or (args_from_time <= from_time and args_to_time >= to_time)
):
return True
return False
def update_cost(self):
for data in self.time_logs:
if data.activity_type or data.is_billable:

View File

@@ -14,12 +14,6 @@
"to_time",
"hours",
"completed",
"section_break_7",
"completed_qty",
"workstation",
"column_break_12",
"operation",
"operation_id",
"project_details",
"project",
"project_name",
@@ -83,43 +77,6 @@
"fieldtype": "Check",
"label": "Completed"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:parent.work_order",
"fieldname": "completed_qty",
"fieldtype": "Float",
"label": "Completed Qty"
},
{
"depends_on": "eval:parent.work_order",
"fieldname": "workstation",
"fieldtype": "Link",
"label": "Workstation",
"options": "Workstation",
"read_only": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:parent.work_order",
"fieldname": "operation",
"fieldtype": "Link",
"label": "Operation",
"options": "Operation",
"read_only": 1
},
{
"depends_on": "eval:parent.work_order",
"fieldname": "operation_id",
"fieldtype": "Data",
"hidden": 1,
"label": "Operation Id"
},
{
"fieldname": "project_details",
"fieldtype": "Section Break"
@@ -267,7 +224,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-05-18 12:19:33.205940",
"modified": "2022-02-17 16:53:34.878798",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet Detail",
@@ -275,5 +232,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

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

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ frappe.query_reports["GSTR-1"] = {
"fieldtype": "Link",
"options": "Address",
"get_query": function () {
var company = frappe.query_report.get_filter_value('company');
let company = frappe.query_report.get_filter_value('company');
if (company) {
return {
"query": 'frappe.contacts.doctype.address.address.address_query',
@@ -26,6 +26,11 @@ frappe.query_reports["GSTR-1"] = {
}
}
},
{
"fieldname": "company_gstin",
"label": __("Company GSTIN"),
"fieldtype": "Select"
},
{
"fieldname": "from_date",
"label": __("From Date"),
@@ -60,10 +65,21 @@ frappe.query_reports["GSTR-1"] = {
}
],
onload: function (report) {
let filters = report.get_values();
frappe.call({
method: 'erpnext.regional.report.gstr_1.gstr_1.get_company_gstins',
args: {
company: filters.company
},
callback: function(r) {
frappe.query_report.page.fields_dict.company_gstin.df.options = r.message;
frappe.query_report.page.fields_dict.company_gstin.refresh();
}
});
report.page.add_inner_button(__("Download as JSON"), function () {
var filters = report.get_values();
frappe.call({
method: 'erpnext.regional.report.gstr_1.gstr_1.get_json',
args: {

View File

@@ -254,7 +254,8 @@ class Gstr1Report(object):
for opts in (("company", " and company=%(company)s"),
("from_date", " and posting_date>=%(from_date)s"),
("to_date", " and posting_date<=%(to_date)s"),
("company_address", " and company_address=%(company_address)s")):
("company_address", " and company_address=%(company_address)s"),
("company_gstin", " and company_gstin=%(company_gstin)s")):
if self.filters.get(opts[0]):
conditions += opts[1]
@@ -1193,3 +1194,23 @@ def is_inter_state(invoice_detail):
return True
else:
return False
@frappe.whitelist()
def get_company_gstins(company):
address = frappe.qb.DocType("Address")
links = frappe.qb.DocType("Dynamic Link")
addresses = frappe.qb.from_(address).inner_join(links).on(
address.name == links.parent
).select(
address.gstin
).where(
links.link_doctype == 'Company'
).where(
links.link_name == company
).run(as_dict=1)
address_list = [''] + [d.gstin for d in addresses]
return address_list

View File

@@ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
var me = this;
var dialog = new frappe.ui.Dialog({
title: __("Select Items"),
size: "large",
fields: [
{
"fieldtype": "Check",
@@ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
} else {
let po_items = [];
me.frm.doc.items.forEach(d => {
let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) {
po_items.push({
"doctype": "Sales Order Item",
@@ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
dialog.show();
},
get_ordered_qty: function(item, so) {
let ordered_qty = item.ordered_qty;
if (so.packed_items) {
// calculate ordered qty based on packed items in case of product bundle
let packed_items = so.packed_items.filter(
(pi) => pi.parent_detail_docname == item.name
);
if (packed_items) {
ordered_qty = packed_items.reduce(
(sum, pi) => sum + flt(pi.ordered_qty),
0
);
ordered_qty = ordered_qty / packed_items.length;
}
}
return ordered_qty;
},
hold_sales_order: function(){
var me = this;
var d = new frappe.ui.Dialog({

View File

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

View File

@@ -921,6 +921,74 @@ class TestSalesOrder(ERPNextTestCase):
self.assertEqual(purchase_orders[0].supplier, '_Test Supplier')
self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1')
def test_product_bundles_in_so_are_replaced_with_bundle_items_in_po(self):
"""
Tests if the the Product Bundles in the Items table of Sales Orders are replaced with
their child items(from the Packed Items table) on creating a Purchase Order from it.
"""
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle",
["_Test Bundle Item 1", "_Test Bundle Item 2"])
so_items = [
{
"item_code": product_bundle.item_code,
"warehouse": "",
"qty": 2,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
}
]
so = make_sales_order(item_list=so_items)
purchase_order = make_purchase_order(so.name, selected_items=so_items)
self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1")
self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2")
def test_purchase_order_updates_packed_item_ordered_qty(self):
"""
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
"""
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle",
["_Test Bundle Item 1", "_Test Bundle Item 2"])
so_items = [
{
"item_code": product_bundle.item_code,
"warehouse": "",
"qty": 2,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
}
]
so = make_sales_order(item_list=so_items)
purchase_order = make_purchase_order(so.name, selected_items=so_items)
purchase_order.supplier = "_Test Supplier"
purchase_order.set_warehouse = "_Test Warehouse - _TC"
purchase_order.save()
purchase_order.submit()
so.reload()
self.assertEqual(so.packed_items[0].ordered_qty, 2)
self.assertEqual(so.packed_items[1].ordered_qty, 2)
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])

View File

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

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

View File

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

View File

@@ -399,6 +399,7 @@ class Item(Document):
if merge:
self.validate_properties_before_merge(new_name)
self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
self.validate_duplicate_website_item_before_merge(old_name, new_name)
def after_rename(self, old_name, new_name, merge):
@@ -463,6 +464,20 @@ class Item(Document):
msg += ": \n" + ", ".join([self.meta.get_label(fld) for fld in field_list])
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
"Block merge if both old and new items have product bundles."
old_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": old_name})
new_bundle = frappe.get_value("Product Bundle",filters={"new_item_code": new_name})
if old_bundle and new_bundle:
bundle_link = get_link_to_form("Product Bundle", old_bundle)
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
msg = _("Please delete Product Bundle {0}, before merging {1} into {2}").format(
bundle_link, old_name, new_name
)
frappe.throw(msg, title=_("Cannot Merge"), exc=DataValidationError)
def validate_duplicate_website_item_before_merge(self, old_name, new_name):
"""
Block merge if both old and new items have website items against them.
@@ -480,8 +495,9 @@ class Item(Document):
old_web_item = [d.get("name") for d in web_items if d.get("item_code") == old_name][0]
web_item_link = get_link_to_form("Website Item", old_web_item)
old_name, new_name = frappe.bold(old_name), frappe.bold(new_name)
msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} and {new_name}"
msg = f"Please delete linked Website Item {frappe.bold(web_item_link)} before merging {old_name} into {new_name}"
frappe.throw(_(msg), title=_("Cannot Merge"), exc=DataValidationError)
def set_last_purchase_rate(self, new_name):

View File

@@ -14,6 +14,7 @@ from erpnext.controllers.item_variant import (
get_variant,
)
from erpnext.stock.doctype.item.item import (
DataValidationError,
InvalidBarcode,
StockExistsForTemplate,
get_item_attribute,
@@ -387,6 +388,26 @@ class TestItem(ERPNextTestCase):
self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"}))
def test_item_merging_with_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
create_item("Test Item Bundle Item 1", is_stock_item=False)
create_item("Test Item Bundle Item 2", is_stock_item=False)
create_item("Test Item inside Bundle")
bundle_items = ["Test Item inside Bundle"]
# make bundles for both items
bundle1 = make_product_bundle("Test Item Bundle Item 1", bundle_items, qty=2)
make_product_bundle("Test Item Bundle Item 2", bundle_items, qty=2)
with self.assertRaises(DataValidationError):
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
bundle1.delete()
frappe.rename_doc("Item", "Test Item Bundle Item 1", "Test Item Bundle Item 2", merge=True)
self.assertFalse(frappe.db.exists("Item", "Test Item Bundle Item 1"))
def test_uom_conversion_factor(self):
if frappe.db.exists('Item', 'Test Item UOM'):
frappe.delete_doc('Item', 'Test Item UOM')

View File

@@ -10,6 +10,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.utils import update_gl_entries_after
from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
@@ -177,6 +178,53 @@ class TestLandedCostVoucher(ERPNextTestCase):
self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0)
self.assertEqual(serial_no.warehouse, "Stores - TCP1")
def test_serialized_lcv_delivered(self):
"""In some cases you'd want to deliver before you can know all the
landed costs, this should be allowed for serial nos too.
Case:
- receipt a serial no @ X rate
- delivery the serial no @ X rate
- add LCV to receipt X + Y
- LCV should be successful
- delivery should reflect X+Y valuation.
"""
serial_no = "LCV_TEST_SR_NO"
item_code = "_Test Serialized Item"
warehouse = "Stores - TCP1"
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse=warehouse, qty=1, rate=200,
item_code=item_code, serial_no=serial_no)
serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate")
# deliver it before creating LCV
dn = create_delivery_note(item_code=item_code,
company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
serial_no=serial_no, qty=1, rate=500,
cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1")
charges = 10
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges)
new_purchase_rate = serial_no_rate + charges
serial_no = frappe.db.get_value("Serial No", serial_no,
["warehouse", "purchase_rate"], as_dict=1)
self.assertEqual(serial_no.purchase_rate, new_purchase_rate)
stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
filters={
"voucher_no": dn.name,
"voucher_type": dn.doctype,
"is_cancelled": 0 # LCV cancels with same name.
},
fieldname="stock_value_difference")
# reposting should update the purchase rate in future delivery
self.assertEqual(stock_value_difference, -new_purchase_rate)
def test_landed_cost_voucher_for_odd_numbers (self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)

View File

@@ -57,14 +57,13 @@ class MaterialRequest(BuyingController):
if actual_so_qty and (flt(so_items[so_no][item]) + already_indented > actual_so_qty):
frappe.throw(_("Material Request of maximum {0} can be made for Item {1} against Sales Order {2}").format(actual_so_qty - already_indented, item, so_no))
# Validate
# ---------------------
def validate(self):
super(MaterialRequest, self).validate()
self.validate_schedule_date()
self.check_for_on_hold_or_closed_status('Sales Order', 'sales_order')
self.validate_uom_is_integer("uom", "qty")
self.validate_material_request_type()
if not self.status:
self.status = "Draft"
@@ -84,6 +83,12 @@ class MaterialRequest(BuyingController):
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_material_request_type(self):
""" Validate fields in accordance with selected type """
if self.material_request_type != "Customer Provided":
self.customer = None
def set_title(self):
'''Set title as comma separated list of items'''
if not self.title:

View File

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

View File

@@ -162,6 +162,15 @@ class TestPurchaseReceipt(ERPNextTestCase):
qty=abs(existing_bin_qty)
)
existing_bin_qty, existing_bin_stock_value = frappe.db.get_value(
"Bin",
{
"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC"
},
["actual_qty", "stock_value"]
)
pr = make_purchase_receipt()
stock_value_difference = frappe.db.get_value(

View File

@@ -9,8 +9,7 @@ from collections import defaultdict
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, floor, flt, nowdate
from six import string_types
from frappe.utils import cint, cstr, floor, flt, nowdate
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_stock_balance
@@ -75,7 +74,7 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
purpose: Purpose of Stock Entry
sync (optional): Sync with client side only for client side calls
"""
if isinstance(items, string_types):
if isinstance(items, str):
items = json.loads(items)
items_not_accomodated, updated_table = [], []
@@ -143,11 +142,44 @@ def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
if items_not_accomodated:
show_unassigned_items_message(items_not_accomodated)
items[:] = updated_table if updated_table else items # modify items table
if updated_table and _items_changed(items, updated_table, doctype):
items[:] = updated_table
frappe.msgprint(_("Applied putaway rules."), alert=True)
if sync and json.loads(sync): # sync with client side
return items
def _items_changed(old, new, doctype: str) -> bool:
""" Check if any items changed by application of putaway rules.
If not, changing item table can have side effects since `name` items also changes.
"""
if len(old) != len(new):
return True
old = [frappe._dict(item) if isinstance(item, dict) else item for item in old]
if doctype == "Stock Entry":
compare_keys = ("item_code", "t_warehouse", "transfer_qty", "serial_no")
sort_key = lambda item: (item.item_code, cstr(item.t_warehouse), # noqa
flt(item.transfer_qty), cstr(item.serial_no))
else:
# purchase receipt / invoice
compare_keys = ("item_code", "warehouse", "stock_qty", "received_qty", "serial_no")
sort_key = lambda item: (item.item_code, cstr(item.warehouse), # noqa
flt(item.stock_qty), flt(item.received_qty), cstr(item.serial_no))
old_sorted = sorted(old, key=sort_key)
new_sorted = sorted(new, key=sort_key)
# Once sorted by all relevant keys both tables should align if they are same.
for old_item, new_item in zip(old_sorted, new_sorted):
for key in compare_keys:
if old_item.get(key) != new_item.get(key):
return True
return False
def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
"""Returns an ordered list of putaway rules to apply on an item."""
filters = {

View File

@@ -35,6 +35,18 @@ class TestPutawayRule(ERPNextTestCase):
new_uom.uom_name = "Bag"
new_uom.save()
def assertUnchangedItemsOnResave(self, doc):
""" Check if same items remain even after reapplication of rules.
This is required since some business logic like subcontracting
depends on `name` of items to be same if item isn't changed.
"""
doc.reload()
old_items = {d.name for d in doc.items}
doc.save()
new_items = {d.name for d in doc.items}
self.assertSetEqual(old_items, new_items)
def test_putaway_rules_priority(self):
"""Test if rule is applied by priority, irrespective of free space."""
rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
@@ -50,6 +62,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(pr.items[1].qty, 100)
self.assertEqual(pr.items[1].warehouse, self.warehouse_2)
self.assertUnchangedItemsOnResave(pr)
pr.delete()
rule_1.delete()
rule_2.delete()
@@ -162,6 +176,8 @@ class TestPutawayRule(ERPNextTestCase):
# leftover space was for 500 kg (0.5 Bag)
# Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
self.assertUnchangedItemsOnResave(pr)
pr.delete()
rule_1.delete()
rule_2.delete()
@@ -196,6 +212,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
self.assertEqual(pr.items[1].putaway_rule, rule_1.name)
self.assertUnchangedItemsOnResave(pr)
pr.delete()
rule_1.delete()
@@ -239,6 +257,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg
self.assertEqual(stock_entry_item.putaway_rule, rule_2.name)
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete()
rule_1.delete()
rule_2.delete()
@@ -294,6 +314,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry.items[2].qty, 200)
self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name)
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete()
rule_1.delete()
rule_2.delete()
@@ -344,6 +366,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:]))
self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete()
pr.cancel()
rule_1.delete()
@@ -366,6 +390,8 @@ class TestPutawayRule(ERPNextTestCase):
self.assertEqual(stock_entry_item.qty, 100)
self.assertEqual(stock_entry_item.putaway_rule, rule_1.name)
self.assertUnchangedItemsOnResave(stock_entry)
stock_entry.delete()
rule_1.delete()
rule_2.delete()

View File

@@ -627,6 +627,12 @@ frappe.ui.form.on('Stock Entry Detail', {
frm.events.set_serial_no(frm, cdt, cdn, () => {
frm.events.get_warehouse_details(frm, cdt, cdn);
});
// set allow_zero_valuation_rate to 0 if s_warehouse is selected.
let item = frappe.get_doc(cdt, cdn);
if (item.s_warehouse) {
item.allow_zero_valuation_rate = 0;
}
},
t_warehouse: function(frm, cdt, cdn) {

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import json
import frappe
from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today
@@ -32,6 +34,27 @@ class TestStockLedgerEntry(ERPNextTestCase):
frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
def assertSLEs(self, doc, expected_sles, sle_filters=None):
""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line"""
filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0}
if sle_filters:
filters.update(sle_filters)
sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters,
order_by="timestamp(posting_date, posting_time), creation")
for exp_sle, act_sle in zip(expected_sles, sles):
for k, v in exp_sle.items():
act_value = act_sle[k]
if k == "stock_queue":
act_value = json.loads(act_value)
if act_value and act_value[0][0] == 0:
# ignore empty fifo bins
continue
self.assertEqual(v, act_value, msg=f"{k} doesn't match \n{exp_sle}\n{act_sle}")
def test_item_cost_reposting(self):
company = "_Test Company"
@@ -349,6 +372,77 @@ class TestStockLedgerEntry(ERPNextTestCase):
frappe.set_user("Administrator")
user.remove_roles("Stock Manager")
def test_fifo_dependent_consumption(self):
item = make_item("_TestFifoTransferRates")
source = "_Test Warehouse - _TC"
target = "Stores - _TC"
rates = [10 * i for i in range(1, 20)]
receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10)
for rate in rates[1:]:
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
row.basic_rate = rate
receipt.append("items", row)
receipt.save()
receipt.submit()
expected_queues = []
for idx, rate in enumerate(rates, start=1):
expected_queues.append(
{"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]}
)
self.assertSLEs(receipt, expected_queues)
transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10)
for rate in rates[1:]:
row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False)
transfer.append("items", row)
transfer.save()
transfer.submit()
# same exact queue should be transferred
self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target})
def test_fifo_multi_item_repack_consumption(self):
rm = make_item("_TestFifoRepackRM")
packed = make_item("_TestFifoRepackFinished")
warehouse = "_Test Warehouse - _TC"
rates = [10 * i for i in range(1, 5)]
receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10)
for rate in rates[1:]:
row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False)
row.basic_rate = rate
receipt.append("items", row)
receipt.save()
receipt.submit()
repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10,
do_not_save=True, rate=10, purpose="Repack")
for rate in rates[1:]:
row = frappe.copy_doc(repack.items[0], ignore_no_copy=False)
repack.append("items", row)
repack.append("items", {
"item_code": packed.name,
"t_warehouse": warehouse,
"qty": 1,
"transfer_qty": 1,
})
repack.save()
repack.submit()
# same exact queue should be transferred
self.assertSLEs(repack, [
{"incoming_rate": sum(rates) * 10}
], sle_filters={"item_code": packed.name})
def create_repack_entry(**args):
args = frappe._dict(args)

View File

@@ -21,6 +21,7 @@ SLE_FIELDS = (
"stock_value",
"stock_value_difference",
"valuation_rate",
"voucher_detail_no",
)
@@ -60,10 +61,15 @@ def add_invariant_check_fields(sles):
fifo_qty += qty
fifo_value += qty * rate
if sle.actual_qty < 0:
sle.consumption_rate = sle.stock_value_difference / sle.actual_qty
balance_qty += sle.actual_qty
balance_stock_value += sle.stock_value_difference
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
balance_qty = sle.qty_after_transaction
balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty")
if balance_qty is None:
balance_qty = sle.qty_after_transaction
sle.fifo_queue_qty = fifo_qty
sle.fifo_stock_value = fifo_value
@@ -145,9 +151,9 @@ def get_columns():
"label": "Incoming Rate",
},
{
"fieldname": "outgoing_rate",
"fieldname": "consumption_rate",
"fieldtype": "Float",
"label": "Outgoing Rate",
"label": "Consumption Rate",
},
{
"fieldname": "qty_after_transaction",

View File

@@ -23,9 +23,18 @@ class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass
_exceptions = frappe.local('stockledger_exceptions')
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
""" Create SL entries from SL entry dicts
args:
- allow_negative_stock: disable negative stock valiations if true
- via_landed_cost_voucher: landed cost voucher cancels and reposts
entries of purchase document. This flag is used to identify if
cancellation and repost is happening via landed cost voucher, in
such cases certain validations need to be ignored (like negative
stock)
"""
from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries:
cancel = sl_entries[0].get("is_cancelled")
@@ -37,7 +46,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
future_sle_exists(args, sl_entries)
for sle in sl_entries:
if sle.serial_no:
if sle.serial_no and not via_landed_cost_voucher:
validate_serial_no(sle)
if cancel:
@@ -459,6 +468,8 @@ class update_entries_after(object):
# rounding as per precision
self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
if not self.wh_data.qty_after_transaction:
self.wh_data.stock_value = 0.0
stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
self.wh_data.prev_stock_value = self.wh_data.stock_value
@@ -622,9 +633,7 @@ class update_entries_after(object):
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def get_incoming_value_for_serial_nos(self, sle, serial_nos):
# get rate from serial nos within same company
@@ -690,9 +699,7 @@ class update_entries_after(object):
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
def get_fifo_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
@@ -723,9 +730,7 @@ class update_entries_after(object):
# Get valuation rate from last sle if exists or from valuation rate field in item master
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
_rate = self.get_fallback_rate(sle)
else:
_rate = 0
@@ -788,6 +793,13 @@ class update_entries_after(object):
else:
return 0
def get_fallback_rate(self, sle) -> float:
"""When exact incoming rate isn't available use any of other "average" rates as fallback.
This should only get used for negative stock."""
return get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company), company=sle.company)
def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket"""
sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)

View File

@@ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase):
Test Stock and Service Item Search.
"""
pos_profile = make_pos_profile()
pos_profile = make_pos_profile(name="Test POS Profile for Search")
item1 = make_item("Test Search Stock Item", {"is_stock_item": 1})
make_stock_entry(
item_code="Test Search Stock Item",

View File

@@ -66,6 +66,20 @@ def create_test_contact_and_address():
contact.add_phone("+91 0000000000", is_primary_phone=True)
contact.insert()
contact_two = frappe.get_doc({
"doctype": 'Contact',
"first_name": "_Test Contact 2 for _Test Customer",
"links": [
{
"link_doctype": "Customer",
"link_name": "_Test Customer"
}
]
})
contact_two.add_email("test_contact_two_customer@example.com", is_primary=True)
contact_two.add_phone("+92 0000000000", is_primary_phone=True)
contact_two.insert()
@contextmanager
def change_settings(doctype, settings_dict):

View File

@@ -3726,7 +3726,7 @@ Earliest Age,Frühestes Alter,
Edit Details,Details bearbeiten,
Edit Profile,Profil bearbeiten,
Either GST Transporter ID or Vehicle No is required if Mode of Transport is Road,Bei Straßentransport ist entweder die GST-Transporter-ID oder die Fahrzeug-Nr. Erforderlich,
Email,Email,
Email,E-Mail,
Email Campaigns,E-Mail-Kampagnen,
Employee ID is linked with another instructor,Die Mitarbeiter-ID ist mit einem anderen Ausbilder verknüpft,
Employee Tax and Benefits,Mitarbeitersteuern und -leistungen,
@@ -6481,7 +6481,7 @@ Select Users,Wählen Sie Benutzer aus,
Send Emails At,Die E-Mails senden um,
Reminder,Erinnerung,
Daily Work Summary Group User,Tägliche Arbeit Zusammenfassung Gruppenbenutzer,
email,Email,
email,E-Mail,
Parent Department,Elternabteilung,
Leave Block List,Urlaubssperrenliste,
Days for which Holidays are blocked for this department.,"Tage, an denen eine Urlaubssperre für diese Abteilung gilt.",
Can't render this file because it is too large.

View File

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