mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-17 05:45:11 +00:00
Merge branch 'version-12-hotfix' into v12-pre-release
This commit is contained in:
@@ -27,4 +27,4 @@ def get_vouchar_detials(column_list, doctype, docname):
|
||||
for col in column_list:
|
||||
sanitize_searchfield(col)
|
||||
return frappe.db.sql(''' select {columns} from `tab{doctype}` where name=%s'''
|
||||
.format(columns=", ".join(json.loads(column_list)), doctype=doctype), docname, as_dict=1)[0]
|
||||
.format(columns=", ".join(column_list), doctype=doctype), docname, as_dict=1)[0]
|
||||
|
||||
@@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document):
|
||||
|
||||
def populate_payment_entries(self):
|
||||
if self.bank_statement is None: return
|
||||
filename = self.bank_statement.split("/")[-1]
|
||||
file_url = self.bank_statement
|
||||
if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0):
|
||||
frappe.throw(_("Transactions already retreived from the statement"))
|
||||
|
||||
@@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document):
|
||||
if self.bank_settings:
|
||||
mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items
|
||||
statement_headers = self.get_statement_headers()
|
||||
transactions = get_transaction_entries(filename, statement_headers)
|
||||
transactions = get_transaction_entries(file_url, statement_headers)
|
||||
for entry in transactions:
|
||||
date = entry[statement_headers["Date"]].strip()
|
||||
#print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"]))
|
||||
@@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row):
|
||||
transaction[header] = ""
|
||||
return transaction
|
||||
|
||||
def get_transaction_entries(filename, headers):
|
||||
def get_transaction_entries(file_url, headers):
|
||||
header_index = {}
|
||||
rows, transactions = [], []
|
||||
|
||||
if (filename.lower().endswith("xlsx")):
|
||||
if (file_url.lower().endswith("xlsx")):
|
||||
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
|
||||
rows = read_xlsx_file_from_attached_file(file_id=filename)
|
||||
elif (filename.lower().endswith("csv")):
|
||||
rows = read_xlsx_file_from_attached_file(file_url=file_url)
|
||||
elif (file_url.lower().endswith("csv")):
|
||||
from frappe.utils.csvutils import read_csv_content
|
||||
_file = frappe.get_doc("File", {"file_name": filename})
|
||||
_file = frappe.get_doc("File", {"file_url": file_url})
|
||||
filepath = _file.get_full_path()
|
||||
with open(filepath,'rb') as csvfile:
|
||||
rows = read_csv_content(csvfile.read())
|
||||
elif (filename.lower().endswith("xls")):
|
||||
elif (file_url.lower().endswith("xls")):
|
||||
filename = file_url.split("/")[-1]
|
||||
rows = get_rows_from_xls_file(filename)
|
||||
else:
|
||||
frappe.throw(_("Only .csv and .xlsx files are supported currently"))
|
||||
|
||||
@@ -619,20 +619,12 @@ $.extend(erpnext.journal_entry, {
|
||||
return { filters: filters };
|
||||
},
|
||||
|
||||
reverse_journal_entry: function(frm) {
|
||||
var me = frm.doc;
|
||||
for(var i=0; i<me.accounts.length; i++) {
|
||||
me.accounts[i].credit += me.accounts[i].debit;
|
||||
me.accounts[i].debit = me.accounts[i].credit - me.accounts[i].debit;
|
||||
me.accounts[i].credit -= me.accounts[i].debit;
|
||||
me.accounts[i].credit_in_account_currency = me.accounts[i].credit;
|
||||
me.accounts[i].debit_in_account_currency = me.accounts[i].debit;
|
||||
me.accounts[i].reference_type = "Journal Entry";
|
||||
me.accounts[i].reference_name = me.name
|
||||
}
|
||||
frm.copy_doc();
|
||||
cur_frm.reload_doc();
|
||||
}
|
||||
reverse_journal_entry: function() {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry",
|
||||
frm: cur_frm
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
$.extend(erpnext.journal_entry, {
|
||||
|
||||
@@ -1017,3 +1017,34 @@ def make_inter_company_journal_entry(name, voucher_type, company):
|
||||
journal_entry.posting_date = nowdate()
|
||||
journal_entry.inter_company_journal_entry_reference = name
|
||||
return journal_entry.as_dict()
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_reverse_journal_entry(source_name, target_doc=None):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
def update_accounts(source, target, source_parent):
|
||||
target.reference_type = "Journal Entry"
|
||||
target.reference_name = source_parent.name
|
||||
|
||||
doclist = get_mapped_doc("Journal Entry", source_name, {
|
||||
"Journal Entry": {
|
||||
"doctype": "Journal Entry",
|
||||
"validation": {
|
||||
"docstatus": ["=", 1]
|
||||
}
|
||||
},
|
||||
"Journal Entry Account": {
|
||||
"doctype": "Journal Entry Account",
|
||||
"field_map": {
|
||||
"account_currency": "account_currency",
|
||||
"exchange_rate": "exchange_rate",
|
||||
"debit_in_account_currency": "credit_in_account_currency",
|
||||
"debit": "credit",
|
||||
"credit_in_account_currency": "debit_in_account_currency",
|
||||
"credit": "debit",
|
||||
},
|
||||
"postprocess": update_accounts,
|
||||
},
|
||||
}, target_doc)
|
||||
|
||||
return doclist
|
||||
@@ -139,6 +139,49 @@ class TestJournalEntry(unittest.TestCase):
|
||||
|
||||
self.assertFalse(gle)
|
||||
|
||||
def test_reverse_journal_entry(self):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
jv = make_journal_entry("_Test Bank USD - _TC",
|
||||
"Sales - _TC", 100, exchange_rate=50, save=False)
|
||||
|
||||
jv.get("accounts")[1].credit_in_account_currency = 5000
|
||||
jv.get("accounts")[1].exchange_rate = 1
|
||||
jv.submit()
|
||||
|
||||
rjv = make_reverse_journal_entry(jv.name)
|
||||
rjv.posting_date = nowdate()
|
||||
rjv.submit()
|
||||
|
||||
|
||||
gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
|
||||
debit_in_account_currency, credit_in_account_currency
|
||||
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
|
||||
order by account asc""", rjv.name, as_dict=1)
|
||||
|
||||
self.assertTrue(gl_entries)
|
||||
|
||||
|
||||
expected_values = {
|
||||
"_Test Bank USD - _TC": {
|
||||
"account_currency": "USD",
|
||||
"debit": 0,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit": 5000,
|
||||
"credit_in_account_currency": 100,
|
||||
},
|
||||
"Sales - _TC": {
|
||||
"account_currency": "INR",
|
||||
"debit": 5000,
|
||||
"debit_in_account_currency": 5000,
|
||||
"credit": 0,
|
||||
"credit_in_account_currency": 0,
|
||||
}
|
||||
}
|
||||
|
||||
for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(expected_values[gle.account][field], gle[field])
|
||||
|
||||
def test_disallow_change_in_account_currency_for_a_party(self):
|
||||
# create jv in USD
|
||||
jv = make_journal_entry("_Test Bank USD - _TC",
|
||||
|
||||
@@ -84,7 +84,7 @@ class PaymentEntry(AccountsController):
|
||||
self.delink_advance_entry_references()
|
||||
self.update_payment_schedule(cancel=1)
|
||||
self.set_payment_req_status()
|
||||
self.set_status()
|
||||
self.set_status(update=True)
|
||||
|
||||
def set_payment_req_status(self):
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
|
||||
@@ -279,7 +279,7 @@ class PaymentEntry(AccountsController):
|
||||
outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"])
|
||||
if outstanding_amount <= 0 and not is_return:
|
||||
no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
|
||||
|
||||
|
||||
for k, v in no_oustanding_refs.items():
|
||||
frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.<br><br>\
|
||||
If this is undesirable please cancel the corresponding Payment Entry.")
|
||||
@@ -340,7 +340,7 @@ class PaymentEntry(AccountsController):
|
||||
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s
|
||||
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
|
||||
|
||||
def set_status(self):
|
||||
def set_status(self, update=False):
|
||||
if self.docstatus == 2:
|
||||
self.status = 'Cancelled'
|
||||
elif self.docstatus == 1:
|
||||
@@ -348,6 +348,9 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
self.status = 'Draft'
|
||||
|
||||
if update:
|
||||
self.db_set('status', self.status)
|
||||
|
||||
def set_amounts(self):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.set_total_allocated_amount()
|
||||
|
||||
@@ -29,27 +29,29 @@ class TestPOSProfile(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def make_pos_profile():
|
||||
def make_pos_profile(**args):
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
pos_profile = frappe.get_doc({
|
||||
"company": "_Test Company",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"currency": "INR",
|
||||
"company": args.company or "_Test Company",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||
"currency": args.currency or "INR",
|
||||
"doctype": "POS Profile",
|
||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||
"income_account": "Sales - _TC",
|
||||
"name": "_Test POS Profile",
|
||||
"expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC",
|
||||
"income_account": args.income_account or "Sales - _TC",
|
||||
"name": args.name or "_Test POS Profile",
|
||||
"naming_series": "_T-POS Profile-",
|
||||
"selling_price_list": "_Test Price List",
|
||||
"territory": "_Test Territory",
|
||||
"selling_price_list": args.selling_price_list or "_Test Price List",
|
||||
"territory": args.territory or "_Test Territory",
|
||||
"customer_group": frappe.db.get_value('Customer Group', {'is_group': 0}, 'name'),
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"write_off_account": "_Test Write Off - _TC",
|
||||
"write_off_cost_center": "_Test Write Off Cost Center - _TC"
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"write_off_account": args.write_off_account or "_Test Write Off - _TC",
|
||||
"write_off_cost_center": args.write_off_cost_center or "_Test Write Off Cost Center - _TC"
|
||||
})
|
||||
|
||||
if not frappe.db.exists("POS Profile", "_Test POS Profile"):
|
||||
if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"):
|
||||
pos_profile.insert()
|
||||
|
||||
return pos_profile
|
||||
|
||||
@@ -13,7 +13,8 @@ def get_data():
|
||||
'Auto Repeat': 'reference_document',
|
||||
},
|
||||
'internal_links': {
|
||||
'Sales Order': ['items', 'sales_order']
|
||||
'Sales Order': ['items', 'sales_order'],
|
||||
'Delivery Note': ['items', 'delivery_note']
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
@@ -33,4 +34,4 @@ def get_data():
|
||||
'items': ['Auto Repeat']
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -714,6 +714,64 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.pos_gl_entry(si, pos, 50)
|
||||
|
||||
def test_pos_returns_without_repayment(self):
|
||||
pos_profile = make_pos_profile()
|
||||
|
||||
pos = create_sales_invoice(qty = 10, do_not_save=True)
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
|
||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
|
||||
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
pos_return = create_sales_invoice(is_return=1,
|
||||
return_against=pos.name, qty=-5, do_not_save=True)
|
||||
|
||||
pos_return.is_pos = 1
|
||||
pos_return.pos_profile = pos_profile.name
|
||||
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
self.assertFalse(pos_return.is_pos)
|
||||
self.assertFalse(pos_return.get('payments'))
|
||||
|
||||
def test_pos_returns_with_repayment(self):
|
||||
pos_profile = make_pos_profile()
|
||||
|
||||
pos_profile.append('payments', {
|
||||
'default': 1,
|
||||
'mode_of_payment': 'Cash',
|
||||
'amount': 0.0
|
||||
})
|
||||
|
||||
pos_profile.save()
|
||||
|
||||
pos = create_sales_invoice(qty = 10, do_not_save=True)
|
||||
|
||||
pos.is_pos = 1
|
||||
pos.pos_profile = pos_profile.name
|
||||
|
||||
pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500})
|
||||
pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500})
|
||||
pos.insert()
|
||||
pos.submit()
|
||||
|
||||
pos_return = create_sales_invoice(is_return=1,
|
||||
return_against=pos.name, qty=-5, do_not_save=True)
|
||||
|
||||
pos_return.is_pos = 1
|
||||
pos_return.pos_profile = pos_profile.name
|
||||
pos_return.insert()
|
||||
pos_return.submit()
|
||||
|
||||
self.assertEqual(pos_return.get('payments')[0].amount, -500)
|
||||
pos_profile.payments = []
|
||||
pos_profile.save()
|
||||
|
||||
|
||||
def test_pos_change_amount(self):
|
||||
make_pos_profile()
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "ACC-SUB-.YYYY.-.#####",
|
||||
"creation": "2017-07-18 17:50:43.967266",
|
||||
"doctype": "DocType",
|
||||
@@ -184,7 +183,8 @@
|
||||
"fieldname": "invoices",
|
||||
"fieldtype": "Table",
|
||||
"label": "Invoices",
|
||||
"options": "Subscription Invoice"
|
||||
"options": "Subscription Invoice",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -197,8 +197,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-01-27 14:37:32.845173",
|
||||
"modified": "2020-08-27 23:30:02.504042",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
|
||||
@@ -326,8 +326,7 @@ class Subscription(Document):
|
||||
|
||||
def is_postpaid_to_invoice(self):
|
||||
return getdate(nowdate()) > getdate(self.current_invoice_end) or \
|
||||
(getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \
|
||||
not self.has_outstanding_invoice()
|
||||
(getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start))
|
||||
|
||||
def is_prepaid_to_invoice(self):
|
||||
if not self.generate_invoice_at_period_start:
|
||||
@@ -337,8 +336,16 @@ class Subscription(Document):
|
||||
return True
|
||||
|
||||
# Check invoice dates and make sure it doesn't have outstanding invoices
|
||||
return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice()
|
||||
|
||||
return getdate(nowdate()) >= getdate(self.current_invoice_start)
|
||||
|
||||
def is_current_invoice_generated(self):
|
||||
invoice = self.get_current_invoice()
|
||||
|
||||
if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def is_current_invoice_paid(self):
|
||||
if self.is_new_subscription():
|
||||
return False
|
||||
@@ -346,7 +353,7 @@ class Subscription(Document):
|
||||
last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice)
|
||||
if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid':
|
||||
return True
|
||||
|
||||
|
||||
return False
|
||||
|
||||
def process_for_active(self):
|
||||
@@ -358,7 +365,8 @@ class Subscription(Document):
|
||||
2. Change the `Subscription` status to 'Past Due Date'
|
||||
3. Change the `Subscription` status to 'Cancelled'
|
||||
"""
|
||||
if not self.is_current_invoice_paid() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
if not self.is_current_invoice_generated() and not self.is_current_invoice_paid() and \
|
||||
(self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
self.generate_invoice()
|
||||
if self.current_invoice_is_past_due():
|
||||
self.status = 'Past Due Date'
|
||||
@@ -369,6 +377,9 @@ class Subscription(Document):
|
||||
if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end):
|
||||
self.cancel_subscription_at_period_end()
|
||||
|
||||
if self.is_current_invoice_generated() and getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
def cancel_subscription_at_period_end(self):
|
||||
"""
|
||||
Called when `Subscription.cancel_at_period_end` is truthy
|
||||
|
||||
@@ -101,19 +101,19 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.delete()
|
||||
|
||||
def test_invoice_is_generated_at_end_of_billing_period(self):
|
||||
start_date = add_to_date(nowdate(), months=-1)
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
subscription.customer = '_Test Customer'
|
||||
subscription.start = '2018-01-01'
|
||||
subscription.start = start_date
|
||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||
subscription.insert()
|
||||
|
||||
self.assertEqual(subscription.status, 'Active')
|
||||
self.assertEqual(subscription.current_invoice_start, '2018-01-01')
|
||||
self.assertEqual(subscription.current_invoice_end, '2018-01-31')
|
||||
self.assertEqual(subscription.current_invoice_start, start_date)
|
||||
self.assertEqual(subscription.current_invoice_end, add_days(nowdate(), -1))
|
||||
subscription.process()
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
self.assertEqual(subscription.current_invoice_start, '2018-01-01')
|
||||
self.assertEqual(subscription.status, 'Past Due Date')
|
||||
subscription.delete()
|
||||
|
||||
@@ -137,7 +137,6 @@ class TestSubscription(unittest.TestCase):
|
||||
subscription.process()
|
||||
|
||||
self.assertEqual(subscription.status, 'Active')
|
||||
self.assertEqual(subscription.current_invoice_start, add_months(subscription.start, 1))
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
subscription.delete()
|
||||
@@ -538,3 +537,23 @@ class TestSubscription(unittest.TestCase):
|
||||
settings.save()
|
||||
|
||||
subscription.delete()
|
||||
|
||||
def test_duplicate_invoice_check(self):
|
||||
subscription = frappe.new_doc('Subscription')
|
||||
subscription.customer = '_Test Customer'
|
||||
subscription.generate_invoice_at_period_start = True
|
||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||
subscription.start = nowdate()
|
||||
subscription.save()
|
||||
|
||||
# Generate invoice for the current invoicing period
|
||||
subscription.process()
|
||||
subscription.load_from_db()
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
# Proccess subscription again for the same period
|
||||
subscription.process()
|
||||
subscription.load_from_db()
|
||||
|
||||
# No new invoice should be created for current period
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
|
||||
@@ -1064,6 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
||||
$(frappe.render_template("pos_item", {
|
||||
item_code: escape(obj.name),
|
||||
item_price: item_price,
|
||||
title: obj.name || obj.item_name,
|
||||
item_name: obj.name === obj.item_name ? "" : obj.item_name,
|
||||
item_image: obj.image,
|
||||
item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj),
|
||||
@@ -1545,6 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
||||
$.each(this.frm.doc.items || [], function (i, d) {
|
||||
$(frappe.render_template("pos_bill_item_new", {
|
||||
item_code: escape(d.item_code),
|
||||
title: d.item_code || d.item_name,
|
||||
item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("<br>" + d.item_name),
|
||||
qty: d.qty,
|
||||
discount_percentage: d.discount_percentage || 0.0,
|
||||
|
||||
@@ -391,7 +391,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup
|
||||
from erpnext.accounts.doctype.tax_rule.tax_rule import get_tax_template, get_party_details
|
||||
args = {
|
||||
party_type.lower(): party,
|
||||
"company": company
|
||||
"company": company
|
||||
}
|
||||
|
||||
if tax_category:
|
||||
|
||||
@@ -14,7 +14,7 @@ import frappe, erpnext
|
||||
from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
from frappe import _
|
||||
from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr)
|
||||
from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint)
|
||||
|
||||
from six import itervalues
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children
|
||||
@@ -43,7 +43,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, periodicity, accumulated_v
|
||||
start_date = year_start_date
|
||||
months = get_months(year_start_date, year_end_date)
|
||||
|
||||
for i in range(math.ceil(months / months_to_add)):
|
||||
for i in range(cint(math.ceil(months / months_to_add))):
|
||||
period = frappe._dict({
|
||||
"from_date": start_date
|
||||
})
|
||||
|
||||
@@ -43,8 +43,11 @@ def execute(filters=None):
|
||||
|
||||
|
||||
def validate_filters(filters, account_details):
|
||||
if not filters.get('company'):
|
||||
frappe.throw(_('{0} is mandatory').format(_('Company')))
|
||||
if not filters.get("company"):
|
||||
frappe.throw(_("{0} is mandatory").format(_("Company")))
|
||||
|
||||
if not filters.get("from_date") and not filters.get("to_date"):
|
||||
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
|
||||
|
||||
if filters.get("account") and not account_details.get(filters.account):
|
||||
frappe.throw(_("Account {0} does not exists").format(filters.account))
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-02-25 17:03:34",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:12:22.464240",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Gross Profit",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Gross Profit",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 1,
|
||||
"creation": "2013-02-25 17:03:34",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2020-08-13 11:26:39.112352",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Gross Profit",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Sales Invoice",
|
||||
"report_name": "Gross Profit",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "Accounts User"
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ class Asset(AccountsController):
|
||||
'assets': assets,
|
||||
'purpose': 'Receipt',
|
||||
'company': self.company,
|
||||
'transaction_date': getdate(nowdate()),
|
||||
'transaction_date': getdate(self.purchase_date),
|
||||
'reference_doctype': reference_doctype,
|
||||
'reference_name': reference_docname
|
||||
}).insert()
|
||||
|
||||
@@ -1055,7 +1055,8 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"modified": "2020-07-01 12:40:45.240948",
|
||||
"links": [],
|
||||
"modified": "2020-09-14 14:36:12.418690",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
@@ -1112,5 +1113,6 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "title"
|
||||
"title_field": "supplier",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.stock.doctype.batch.test_batch import make_new_batch
|
||||
from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
|
||||
|
||||
class TestPurchaseOrder(unittest.TestCase):
|
||||
def test_make_purchase_receipt(self):
|
||||
@@ -580,7 +582,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
|
||||
def test_exploded_items_in_subcontracted(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code)
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=1,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
@@ -602,7 +604,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
|
||||
def test_backflush_based_on_stock_entry(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code)
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
make_item('Sub Contracted Raw Material 1', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
@@ -661,6 +663,76 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_backflushed_based_on_for_multiple_batches(self):
|
||||
item_code = "_Test Subcontracted FG Item 2"
|
||||
make_item('Sub Contracted Raw Material 2', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
})
|
||||
|
||||
make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
|
||||
raw_materials=["Sub Contracted Raw Material 2"])
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
|
||||
order_qty = 500
|
||||
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
|
||||
"qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.submit()
|
||||
|
||||
for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
|
||||
make_new_batch(batch_id=batch, item_code=item_code)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
|
||||
# partial receipt
|
||||
pr.get('items')[0].qty = 30
|
||||
pr.get('items')[0].batch_no = "ABCD1"
|
||||
|
||||
purchase_order = po.name
|
||||
purchase_order_item = po.items[0].name
|
||||
|
||||
for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
|
||||
pr.append("items", {
|
||||
"item_code": pr.get('items')[0].item_code,
|
||||
"item_name": pr.get('items')[0].item_name,
|
||||
"uom": pr.get('items')[0].uom,
|
||||
"stock_uom": pr.get('items')[0].stock_uom,
|
||||
"warehouse": pr.get('items')[0].warehouse,
|
||||
"conversion_factor": pr.get('items')[0].conversion_factor,
|
||||
"cost_center": pr.get('items')[0].cost_center,
|
||||
"rate": pr.get('items')[0].rate,
|
||||
"qty": qty,
|
||||
"batch_no": batch_no,
|
||||
"purchase_order": purchase_order,
|
||||
"purchase_order_item": purchase_order_item
|
||||
})
|
||||
|
||||
pr.submit()
|
||||
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr1.get('items')[0].qty = 300
|
||||
pr1.get('items')[0].batch_no = "ABCD1"
|
||||
pr1.save()
|
||||
|
||||
pr_key = ("Sub Contracted Raw Material 2", po.name)
|
||||
consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
|
||||
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
|
||||
self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def test_advance_payment_entry_unlink_against_purchase_order(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
||||
@@ -712,27 +784,33 @@ def make_pr_against_po(po, received_qty=0):
|
||||
pr.submit()
|
||||
return pr
|
||||
|
||||
def make_subcontracted_item(item_code):
|
||||
def make_subcontracted_item(**args):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
if not frappe.db.exists('Item', item_code):
|
||||
make_item(item_code, {
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not frappe.db.exists('Item', args.item_code):
|
||||
make_item(args.item_code, {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1
|
||||
'is_sub_contracted_item': 1,
|
||||
'has_batch_no': args.get("has_batch_no") or 0
|
||||
})
|
||||
|
||||
if not frappe.db.exists('Item', "Test Extra Item 1"):
|
||||
make_item("Test Extra Item 1", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
if not args.raw_materials:
|
||||
if not frappe.db.exists('Item', "Test Extra Item 1"):
|
||||
make_item("Test Extra Item 1", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
|
||||
if not frappe.db.exists('Item', "Test Extra Item 2"):
|
||||
make_item("Test Extra Item 2", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
if not frappe.db.exists('Item', "Test Extra Item 2"):
|
||||
make_item("Test Extra Item 2", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
|
||||
if not frappe.db.get_value('BOM', {'item': item_code}, 'name'):
|
||||
make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1'])
|
||||
args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
|
||||
|
||||
if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'):
|
||||
make_bom(item = args.item_code, raw_materials = args.get("raw_materials"))
|
||||
|
||||
def update_backflush_based_on(based_on):
|
||||
doc = frappe.get_doc('Buying Settings')
|
||||
|
||||
@@ -7,6 +7,7 @@ import json
|
||||
from frappe import _, throw
|
||||
from frappe.utils import (today, flt, cint, fmt_money, formatdate,
|
||||
getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form)
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError
|
||||
from erpnext.stock.get_item_details import get_conversion_factor, get_item_details
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency
|
||||
@@ -1163,7 +1164,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
|
||||
child_item.base_amount = 1 # Initiallize value will update in parent validation
|
||||
return child_item
|
||||
|
||||
def check_and_delete_children(parent, data):
|
||||
def validate_and_delete_children(parent, data):
|
||||
deleted_children = []
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
for item in parent.items:
|
||||
@@ -1190,18 +1191,37 @@ def check_and_delete_children(parent, data):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
def check_permissions(doc, perm_type='create'):
|
||||
def check_doc_permissions(doc, perm_type='create'):
|
||||
try:
|
||||
doc.check_permission(perm_type)
|
||||
except:
|
||||
action = "add" if perm_type == 'create' else "update"
|
||||
frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions"))
|
||||
except frappe.PermissionError:
|
||||
actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' }
|
||||
|
||||
frappe.throw(_("You do not have permissions to {} items in a {}.")
|
||||
.format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions"))
|
||||
|
||||
def validate_workflow_conditions(doc):
|
||||
workflow = get_workflow_name(doc.doctype)
|
||||
if not workflow:
|
||||
return
|
||||
|
||||
workflow_doc = frappe.get_doc("Workflow", workflow)
|
||||
current_state = doc.get(workflow_doc.workflow_state_field)
|
||||
roles = frappe.get_roles()
|
||||
|
||||
transitions = []
|
||||
for transition in workflow_doc.transitions:
|
||||
if transition.next_state == current_state and transition.allowed in roles:
|
||||
if not is_transition_condition_satisfied(transition, doc):
|
||||
continue
|
||||
transitions.append(transition.as_dict())
|
||||
|
||||
if not transitions:
|
||||
frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions"))
|
||||
|
||||
def get_new_child_item(item_row):
|
||||
if parent_doctype == "Sales Order":
|
||||
return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
|
||||
if parent_doctype == "Purchase Order":
|
||||
return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
|
||||
new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
|
||||
return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row)
|
||||
|
||||
def validate_quantity(child_item, d):
|
||||
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
|
||||
@@ -1214,17 +1234,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_and_delete_children(parent, data)
|
||||
|
||||
check_doc_permissions(parent, 'cancel')
|
||||
validate_and_delete_children(parent, data)
|
||||
|
||||
for d in data:
|
||||
new_child_flag = False
|
||||
if not d.get("docname"):
|
||||
new_child_flag = True
|
||||
check_permissions(parent, 'create')
|
||||
check_doc_permissions(parent, 'create')
|
||||
child_item = get_new_child_item(d)
|
||||
else:
|
||||
check_permissions(parent, 'write')
|
||||
check_doc_permissions(parent, 'write')
|
||||
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
|
||||
|
||||
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
|
||||
@@ -1331,6 +1352,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_prevdoc_status('submit')
|
||||
parent.update_delivery_status()
|
||||
|
||||
parent.reload()
|
||||
validate_workflow_conditions(parent)
|
||||
|
||||
parent.update_blanket_order()
|
||||
parent.update_billing_percentage()
|
||||
parent.set_status()
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.utils import flt,cint, cstr, getdate
|
||||
|
||||
from six import iteritems
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
|
||||
@@ -267,6 +267,9 @@ class BuyingController(StockController):
|
||||
qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
|
||||
|
||||
for item in self.get('items'):
|
||||
if not item.purchase_order:
|
||||
continue
|
||||
|
||||
# reset raw_material cost
|
||||
item.rm_supp_cost = 0
|
||||
|
||||
@@ -279,11 +282,17 @@ class BuyingController(StockController):
|
||||
|
||||
fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
|
||||
|
||||
if not fg_yet_to_be_received:
|
||||
frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
|
||||
.format(item.idx, frappe.bold(item.item_code),
|
||||
frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
|
||||
title=_("Limit Crossed"))
|
||||
|
||||
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
|
||||
backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
|
||||
|
||||
for raw_material in transferred_raw_materials + non_stock_items:
|
||||
rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order)
|
||||
rm_item_key = (raw_material.rm_item_code, item.purchase_order)
|
||||
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
||||
|
||||
consumed_qty = raw_material_data.get('qty', 0)
|
||||
@@ -313,7 +322,7 @@ class BuyingController(StockController):
|
||||
|
||||
if raw_material.batch_nos:
|
||||
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
|
||||
qty, transferred_batch_qty_map, backflushed_batch_qty_map)
|
||||
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
|
||||
for batch_data in batches_qty:
|
||||
qty = batch_data['qty']
|
||||
raw_material.batch_no = batch_data['batch']
|
||||
@@ -325,6 +334,10 @@ class BuyingController(StockController):
|
||||
rm = self.append('supplied_items', {})
|
||||
rm.update(raw_material_data)
|
||||
|
||||
if not rm.main_item_code:
|
||||
rm.main_item_code = fg_item_doc.item_code
|
||||
|
||||
rm.reference_name = fg_item_doc.name
|
||||
rm.required_qty = qty
|
||||
rm.consumed_qty = qty
|
||||
|
||||
@@ -835,7 +848,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND IFNULL(sed.t_warehouse, '') != ''
|
||||
AND sed.subcontracted_item = %s
|
||||
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
|
||||
GROUP BY sed.item_code, sed.subcontracted_item
|
||||
"""
|
||||
raw_materials = frappe.db.multisql({
|
||||
@@ -852,39 +865,42 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
|
||||
return raw_materials
|
||||
|
||||
def get_backflushed_subcontracted_raw_materials(purchase_orders):
|
||||
common_query = """
|
||||
SELECT
|
||||
CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key,
|
||||
SUM(prsi.consumed_qty) AS qty,
|
||||
{serial_no_concat_syntax} AS serial_nos,
|
||||
{batch_no_concat_syntax} AS batch_nos
|
||||
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi
|
||||
WHERE
|
||||
pr.name = pri.parent
|
||||
AND pr.name = prsi.parent
|
||||
AND pri.purchase_order IN %s
|
||||
AND pri.item_code = prsi.main_item_code
|
||||
AND pr.docstatus = 1
|
||||
GROUP BY prsi.rm_item_code, pri.purchase_order
|
||||
"""
|
||||
purchase_receipts = frappe.get_all("Purchase Receipt Item",
|
||||
fields = ["purchase_order", "item_code", "name", "parent"],
|
||||
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
|
||||
|
||||
backflushed_raw_materials = frappe.db.multisql({
|
||||
'mariadb': common_query.format(
|
||||
serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)",
|
||||
batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)"
|
||||
),
|
||||
'postgres': common_query.format(
|
||||
serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')",
|
||||
batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')"
|
||||
)
|
||||
}, (purchase_orders, ), as_dict=1)
|
||||
distinct_purchase_receipts = {}
|
||||
for pr in purchase_receipts:
|
||||
key = (pr.purchase_order, pr.item_code, pr.parent)
|
||||
distinct_purchase_receipts.setdefault(key, []).append(pr.name)
|
||||
|
||||
backflushed_raw_materials_map = frappe._dict()
|
||||
for item in backflushed_raw_materials:
|
||||
backflushed_raw_materials_map.setdefault(item.item_key, item)
|
||||
for args, references in iteritems(distinct_purchase_receipts):
|
||||
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
|
||||
|
||||
for data in purchase_receipt_supplied_items:
|
||||
pr_key = (data.rm_item_code, args[0])
|
||||
if pr_key not in backflushed_raw_materials_map:
|
||||
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
|
||||
"qty": 0.0,
|
||||
"serial_no": [],
|
||||
"batch_no": []
|
||||
}))
|
||||
|
||||
row = backflushed_raw_materials_map.get(pr_key)
|
||||
row.qty += data.consumed_qty
|
||||
|
||||
for field in ["serial_no", "batch_no"]:
|
||||
if data.get(field):
|
||||
row[field].append(data.get(field))
|
||||
|
||||
return backflushed_raw_materials_map
|
||||
|
||||
def get_supplied_items(item_code, purchase_receipt, references):
|
||||
return frappe.get_all("Purchase Receipt Item Supplied",
|
||||
fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"],
|
||||
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
|
||||
|
||||
def get_asset_item_details(asset_items):
|
||||
asset_items_data = {}
|
||||
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
|
||||
@@ -966,14 +982,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
SELECT
|
||||
sed.batch_no,
|
||||
SUM(sed.qty) AS qty,
|
||||
sed.item_code
|
||||
sed.item_code,
|
||||
sed.subcontracted_item
|
||||
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
|
||||
WHERE
|
||||
se.name = sed.parent
|
||||
AND se.docstatus=1
|
||||
AND se.purpose='Send to Subcontractor'
|
||||
AND se.purchase_order = %s
|
||||
AND sed.subcontracted_item = %s
|
||||
AND ifnull(sed.subcontracted_item, '') in ('', %s)
|
||||
AND sed.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
sed.batch_no,
|
||||
@@ -981,8 +998,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in transferred_batches:
|
||||
transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
|
||||
transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
|
||||
key = ((batch_data.item_code, fg_item)
|
||||
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
|
||||
transferred_batch_qty_map.setdefault(key, {})
|
||||
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return transferred_batch_qty_map
|
||||
|
||||
@@ -1019,9 +1038,12 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item):
|
||||
|
||||
return backflushed_batch_qty_map
|
||||
|
||||
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map):
|
||||
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map, po):
|
||||
# Returns available batches to be backflushed based on requirements
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
|
||||
if not transferred_batches:
|
||||
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
|
||||
|
||||
backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
|
||||
|
||||
available_batches = []
|
||||
|
||||
@@ -613,9 +613,12 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
if not taxes:
|
||||
return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
|
||||
else:
|
||||
valid_from = filters.get('valid_from')
|
||||
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
|
||||
|
||||
args = {
|
||||
'item_code': filters.get('item_code'),
|
||||
'posting_date': filters.get('valid_from'),
|
||||
'posting_date': valid_from,
|
||||
'tax_category': filters.get('tax_category')
|
||||
}
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ class StatusUpdater(Document):
|
||||
args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
|
||||
from `tab%(second_source_dt)s`
|
||||
where `%(second_join_field)s`="%(detail_id)s"
|
||||
and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args
|
||||
and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0) """ % args
|
||||
|
||||
if args['detail_id']:
|
||||
if not args.get("extra_cond"): args["extra_cond"] = ""
|
||||
|
||||
@@ -242,10 +242,11 @@ class StockController(AccountsController):
|
||||
_(self.doctype), self.name, item.get("item_code")))
|
||||
|
||||
def delete_auto_created_batches(self):
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
for d in self.items:
|
||||
if not d.batch_no: continue
|
||||
|
||||
serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})]
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
if serial_nos:
|
||||
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
|
||||
|
||||
|
||||
@@ -525,7 +525,7 @@ class calculate_taxes_and_totals(object):
|
||||
if self.doc.doctype == "Sales Invoice":
|
||||
self.calculate_paid_amount()
|
||||
|
||||
if self.doc.is_return and self.doc.return_against: return
|
||||
if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return
|
||||
|
||||
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
|
||||
self._set_in_company_currency(self.doc, ['write_off_amount'])
|
||||
@@ -543,7 +543,7 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.round_floats_in(self.doc, ["paid_amount"])
|
||||
change_amount = 0
|
||||
|
||||
if self.doc.doctype == "Sales Invoice":
|
||||
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
|
||||
self.calculate_write_off_amount()
|
||||
self.calculate_change_amount()
|
||||
change_amount = self.doc.change_amount \
|
||||
@@ -555,6 +555,9 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount),
|
||||
self.doc.precision("outstanding_amount"))
|
||||
|
||||
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
|
||||
self.update_paid_amount_for_return(total_amount_to_pay)
|
||||
|
||||
def calculate_paid_amount(self):
|
||||
|
||||
paid_amount = base_paid_amount = 0.0
|
||||
@@ -625,6 +628,27 @@ class calculate_taxes_and_totals(object):
|
||||
def set_item_wise_tax_breakup(self):
|
||||
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
|
||||
|
||||
def update_paid_amount_for_return(self, total_amount_to_pay):
|
||||
default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
|
||||
{'parent': self.doc.pos_profile, 'default': 1},
|
||||
['mode_of_payment', 'type', 'account'], as_dict=1)
|
||||
|
||||
self.doc.payments = []
|
||||
|
||||
if default_mode_of_payment:
|
||||
self.doc.append('payments', {
|
||||
'mode_of_payment': default_mode_of_payment.mode_of_payment,
|
||||
'type': default_mode_of_payment.type,
|
||||
'account': default_mode_of_payment.account,
|
||||
'amount': total_amount_to_pay
|
||||
})
|
||||
else:
|
||||
self.doc.is_pos = 0
|
||||
self.doc.pos_profile = ''
|
||||
|
||||
self.calculate_paid_amount()
|
||||
|
||||
|
||||
def get_itemised_tax_breakup_html(doc):
|
||||
if not doc.taxes:
|
||||
return
|
||||
|
||||
@@ -71,7 +71,7 @@ class ProgramEnrollment(Document):
|
||||
def create_course_enrollments(self):
|
||||
student = frappe.get_doc("Student", self.student)
|
||||
program = frappe.get_doc("Program", self.program)
|
||||
course_list = [course.course for course in program.get_all_children()]
|
||||
course_list = [course.course for course in program.courses]
|
||||
for course_name in course_list:
|
||||
student.enroll_in_course(course_name=course_name, program_enrollment=self.name)
|
||||
|
||||
|
||||
@@ -149,7 +149,9 @@ var check_and_set_availability = function(frm) {
|
||||
primary_action: function() {
|
||||
frm.set_value('appointment_time', selected_slot);
|
||||
frm.set_value('service_unit', service_unit || '');
|
||||
frm.set_value('duration', duration);
|
||||
if (!frm.doc.duration) {
|
||||
frm.set_value('duration', duration);
|
||||
}
|
||||
frm.set_value('practitioner', d.get_value('practitioner'));
|
||||
frm.set_value('department', d.get_value('department'));
|
||||
frm.set_value('appointment_date', d.get_value('appointment_date'));
|
||||
|
||||
@@ -82,7 +82,8 @@ def add_attendance(events, start, end, conditions=None):
|
||||
e = {
|
||||
"name": d.name,
|
||||
"doctype": "Attendance",
|
||||
"date": d.attendance_date,
|
||||
"start": d.attendance_date,
|
||||
"end": d.attendance_date,
|
||||
"title": cstr(d.status),
|
||||
"docstatus": d.docstatus
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
frappe.views.calendar["Attendance"] = {
|
||||
field_map: {
|
||||
"start": "attendance_date",
|
||||
"end": "attendance_date",
|
||||
"id": "name",
|
||||
"docstatus": 1
|
||||
},
|
||||
options: {
|
||||
header: {
|
||||
left: 'prev,next today',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
frappe.listview_settings['Expense Claim'] = {
|
||||
add_fields: ["total_claimed_amount", "docstatus"],
|
||||
add_fields: ["total_claimed_amount", "docstatus", "company"],
|
||||
get_indicator: function(doc) {
|
||||
if(doc.status == "Paid") {
|
||||
return [__("Paid"), "green", "status,=,Paid"];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-02-20 11:18:11",
|
||||
@@ -167,6 +166,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Open",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
@@ -246,9 +246,8 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"max_attachments": 3,
|
||||
"modified": "2020-03-10 22:40:43.487721",
|
||||
"modified": "2020-08-13 17:22:44.832397",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Application",
|
||||
@@ -333,4 +332,4 @@
|
||||
"sort_order": "DESC",
|
||||
"timeline_field": "employee",
|
||||
"title_field": "employee_name"
|
||||
}
|
||||
}
|
||||
@@ -433,6 +433,8 @@ def get_leave_details(employee, date):
|
||||
'from_date': ('<=', date),
|
||||
'to_date': ('>=', date),
|
||||
'leave_type': allocation.leave_type,
|
||||
'employee': employee,
|
||||
'docstatus': 1
|
||||
}, 'SUM(total_leaves_allocated)') or 0
|
||||
|
||||
remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
|
||||
@@ -597,7 +599,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date):
|
||||
is_carry_forward, is_expired
|
||||
FROM `tabLeave Ledger Entry`
|
||||
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
|
||||
AND docstatus=1
|
||||
AND docstatus=1
|
||||
AND (leaves<0
|
||||
OR is_expired=1)
|
||||
AND (from_date between %(from_date)s AND %(to_date)s
|
||||
@@ -790,4 +792,4 @@ def get_leave_approver(employee):
|
||||
leave_approver = frappe.db.get_value('Department Approver', {'parent': department,
|
||||
'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
|
||||
|
||||
return leave_approver
|
||||
return leave_approver
|
||||
|
||||
@@ -290,6 +290,7 @@ class PayrollEntry(Document):
|
||||
"account": default_payroll_payable_account,
|
||||
"credit_in_account_currency": flt(payable_amount, precision),
|
||||
"party_type": '',
|
||||
"cost_center": self.cost_center
|
||||
})
|
||||
|
||||
journal_entry.set("accounts", accounts)
|
||||
|
||||
@@ -54,7 +54,7 @@ class MaintenanceSchedule(TransactionBase):
|
||||
email_map[d.sales_person] = sp.get_email_id()
|
||||
except frappe.ValidationError:
|
||||
no_email_sp.append(d.sales_person)
|
||||
|
||||
|
||||
if no_email_sp:
|
||||
frappe.msgprint(
|
||||
frappe._("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format(
|
||||
@@ -66,17 +66,17 @@ class MaintenanceSchedule(TransactionBase):
|
||||
parent=%s""", (d.sales_person, d.item_code, self.name), as_dict=1)
|
||||
|
||||
for key in scheduled_date:
|
||||
description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer)
|
||||
frappe.get_doc({
|
||||
description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer)
|
||||
event = frappe.get_doc({
|
||||
"doctype": "Event",
|
||||
"owner": email_map.get(d.sales_person, self.owner),
|
||||
"subject": description,
|
||||
"description": description,
|
||||
"starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
|
||||
"event_type": "Private",
|
||||
"ref_type": self.doctype,
|
||||
"ref_name": self.name
|
||||
}).insert(ignore_permissions=1)
|
||||
})
|
||||
event.add_participant(self.doctype, self.name)
|
||||
event.insert(ignore_permissions=1)
|
||||
|
||||
frappe.db.set(self, 'status', 'Submitted')
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
from frappe.utils.data import get_datetime, add_days
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
@@ -9,4 +10,39 @@ import unittest
|
||||
# test_records = frappe.get_test_records('Maintenance Schedule')
|
||||
|
||||
class TestMaintenanceSchedule(unittest.TestCase):
|
||||
pass
|
||||
def test_events_should_be_created_and_deleted(self):
|
||||
ms = make_maintenance_schedule()
|
||||
ms.generate_schedule()
|
||||
ms.submit()
|
||||
|
||||
all_events = get_events(ms)
|
||||
self.assertTrue(len(all_events) > 0)
|
||||
|
||||
ms.cancel()
|
||||
events_after_cancel = get_events(ms)
|
||||
self.assertTrue(len(events_after_cancel) == 0)
|
||||
|
||||
def get_events(ms):
|
||||
return frappe.get_all("Event Participants", filters={
|
||||
"reference_doctype": ms.doctype,
|
||||
"reference_docname": ms.name,
|
||||
"parenttype": "Event"
|
||||
})
|
||||
|
||||
def make_maintenance_schedule():
|
||||
ms = frappe.new_doc("Maintenance Schedule")
|
||||
ms.company = "_Test Company"
|
||||
ms.customer = "_Test Customer"
|
||||
ms.transaction_date = get_datetime()
|
||||
|
||||
ms.append("items", {
|
||||
"item_code": "_Test Item",
|
||||
"start_date": get_datetime(),
|
||||
"end_date": add_days(get_datetime(), 32),
|
||||
"periodicity": "Weekly",
|
||||
"no_of_visits": 4,
|
||||
"sales_person": "Sales Team",
|
||||
})
|
||||
ms.insert(ignore_permissions=True)
|
||||
|
||||
return ms
|
||||
|
||||
@@ -539,7 +539,7 @@ class BOM(WebsiteGenerator):
|
||||
'image' : d.image,
|
||||
'stock_uom' : d.stock_uom,
|
||||
'stock_qty' : flt(d.stock_qty),
|
||||
'rate' : flt(d.base_rate) / flt(d.conversion_factor),
|
||||
'rate' : flt(d.base_rate) / (flt(d.conversion_factor) or 1.0),
|
||||
'include_item_in_manufacturing': d.include_item_in_manufacturing
|
||||
}))
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ def update_latest_price_in_all_boms():
|
||||
update_cost()
|
||||
|
||||
def replace_bom(args):
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
args = frappe._dict(args)
|
||||
|
||||
doc = frappe.get_doc("BOM Update Tool")
|
||||
@@ -97,6 +98,8 @@ def replace_bom(args):
|
||||
doc.new_bom = args.new_bom
|
||||
doc.replace_bom()
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
||||
def update_cost():
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
bom_list = get_boms_in_bottom_up_order()
|
||||
|
||||
@@ -2,6 +2,17 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Job Card', {
|
||||
setup: function(frm) {
|
||||
frm.set_query('operation', function() {
|
||||
return {
|
||||
query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations',
|
||||
filters: {
|
||||
'work_order': frm.doc.work_order
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
|
||||
if(frm.doc.docstatus == 0) {
|
||||
@@ -24,12 +35,60 @@ frappe.ui.form.on('Job Card', {
|
||||
}
|
||||
}
|
||||
|
||||
frm.trigger("toggle_operation_number");
|
||||
|
||||
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
||||
&& (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
},
|
||||
|
||||
operation: function(frm) {
|
||||
frm.trigger("toggle_operation_number");
|
||||
|
||||
if (frm.doc.operation && frm.doc.work_order) {
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details",
|
||||
args: {
|
||||
"work_order":frm.doc.work_order,
|
||||
"operation":frm.doc.operation
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.message) {
|
||||
if (r.message.length == 1) {
|
||||
frm.set_value("operation_id", r.message[0].name);
|
||||
} else {
|
||||
let args = [];
|
||||
|
||||
r.message.forEach((row) => {
|
||||
args.push({ "label": row.idx, "value": row.name });
|
||||
});
|
||||
|
||||
let description = __("Operation {0} added multiple times in the work order {1}",
|
||||
[frm.doc.operation, frm.doc.work_order]);
|
||||
|
||||
frm.set_df_property("operation_row_number", "options", args);
|
||||
frm.set_df_property("operation_row_number", "description", description);
|
||||
}
|
||||
|
||||
frm.trigger("toggle_operation_number");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
operation_row_number(frm) {
|
||||
if (frm.doc.operation_row_number) {
|
||||
frm.set_value("operation_id", frm.doc.operation_row_number);
|
||||
}
|
||||
},
|
||||
|
||||
toggle_operation_number(frm) {
|
||||
frm.toggle_display("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
|
||||
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
|
||||
},
|
||||
|
||||
prepare_timer_buttons: function(frm) {
|
||||
frm.trigger("make_dashboard");
|
||||
if (!frm.doc.job_started) {
|
||||
@@ -39,9 +98,9 @@ frappe.ui.form.on('Job Card', {
|
||||
fieldname: 'employee'}, d => {
|
||||
if (d.employee) {
|
||||
frm.set_value("employee", d.employee);
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
}
|
||||
|
||||
frm.events.start_job(frm);
|
||||
}, __("Enter Value"), __("Start"));
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
@@ -86,9 +145,7 @@ frappe.ui.form.on('Job Card', {
|
||||
frm.set_value('current_time' , 0);
|
||||
}
|
||||
|
||||
frm.save("Save", () => {}, "", () => {
|
||||
frm.doc.time_logs.pop(-1);
|
||||
});
|
||||
frm.save();
|
||||
},
|
||||
|
||||
complete_job: function(frm, completed_time, completed_qty) {
|
||||
@@ -120,6 +177,8 @@ frappe.ui.form.on('Job Card', {
|
||||
employee: function(frm) {
|
||||
if (frm.doc.job_started && !frm.doc.current_time) {
|
||||
frm.trigger("reset_timer");
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"bom_no",
|
||||
"workstation",
|
||||
"operation",
|
||||
"operation_row_number",
|
||||
"column_break_4",
|
||||
"posting_date",
|
||||
"company",
|
||||
@@ -287,10 +288,15 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_row_number",
|
||||
"fieldtype": "Select",
|
||||
"label": "Operation Row Number"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"modified": "2020-03-27 13:36:35.417502",
|
||||
"modified": "2020-08-24 15:21:21.398267",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
@@ -342,7 +348,6 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "operation",
|
||||
|
||||
@@ -9,10 +9,13 @@ from frappe.utils import flt, time_diff_in_hours, get_datetime, time_diff, get_l
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.document import Document
|
||||
|
||||
class OperationMismatchError(frappe.ValidationError): pass
|
||||
|
||||
class JobCard(Document):
|
||||
def validate(self):
|
||||
self.validate_time_logs()
|
||||
self.set_status()
|
||||
self.validate_operation_id()
|
||||
|
||||
def validate_time_logs(self):
|
||||
self.total_completed_qty = 0.0
|
||||
@@ -107,11 +110,10 @@ class JobCard(Document):
|
||||
for_quantity, time_in_mins = 0, 0
|
||||
from_time_list, to_time_list = [], []
|
||||
|
||||
field = "operation_id" if self.operation_id else "operation"
|
||||
field = "operation_id"
|
||||
data = frappe.get_all('Job Card',
|
||||
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
|
||||
filters = {"docstatus": 1, "work_order": self.work_order,
|
||||
"workstation": self.workstation, field: self.get(field)})
|
||||
filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)})
|
||||
|
||||
if data and len(data) > 0:
|
||||
for_quantity = data[0].completed_qty
|
||||
@@ -124,14 +126,13 @@ class JobCard(Document):
|
||||
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
||||
WHERE
|
||||
jctl.parent = jc.name and jc.work_order = %s
|
||||
and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1
|
||||
""".format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1)
|
||||
and jc.{0} = %s and jc.docstatus = 1
|
||||
""".format(field), (self.work_order, self.get(field)), as_dict=1)
|
||||
|
||||
wo = frappe.get_doc('Work Order', self.work_order)
|
||||
|
||||
work_order_field = "name" if field == "operation_id" else field
|
||||
for data in wo.operations:
|
||||
if data.get(work_order_field) == self.get(field):
|
||||
if data.get("name") == self.get(field):
|
||||
data.completed_qty = for_quantity
|
||||
data.actual_operation_time = time_in_mins
|
||||
data.actual_start_time = time_data[0].start_time if time_data else None
|
||||
@@ -204,6 +205,37 @@ class JobCard(Document):
|
||||
if update_status:
|
||||
self.db_set('status', self.status)
|
||||
|
||||
def validate_operation_id(self):
|
||||
if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and
|
||||
frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id):
|
||||
work_order = frappe.bold(get_link_to_form("Work Order", self.work_order))
|
||||
frappe.throw(_("Operation {0} does not belong to the work order {1}")
|
||||
.format(frappe.bold(self.operation), work_order), OperationMismatchError)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_operation_details(work_order, operation):
|
||||
if work_order and operation:
|
||||
return frappe.get_all("Work Order Operation", fields = ["name", "idx"],
|
||||
filters = {
|
||||
"parent": work_order,
|
||||
"operation": operation
|
||||
}
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_operations(doctype, txt, searchfield, start, page_len, filters):
|
||||
if filters.get("work_order"):
|
||||
args = {"parent": filters.get("work_order")}
|
||||
if txt:
|
||||
args["operation"] = ("like", "%{0}%".format(txt))
|
||||
|
||||
return frappe.get_all("Work Order Operation",
|
||||
filters = args,
|
||||
fields = ["distinct operation as operation"],
|
||||
limit_start = start,
|
||||
limit_page_length = page_len,
|
||||
order_by="idx asc", as_list=1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_material_request(source_name, target_doc=None):
|
||||
def update_item(obj, target, source_parent):
|
||||
|
||||
@@ -4,6 +4,64 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
import frappe
|
||||
from frappe.utils import random_string
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError
|
||||
|
||||
class TestJobCard(unittest.TestCase):
|
||||
pass
|
||||
def test_job_card(self):
|
||||
data = frappe.get_cached_value('BOM',
|
||||
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
|
||||
|
||||
if data:
|
||||
bom, bom_item = data
|
||||
|
||||
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
|
||||
|
||||
job_cards = frappe.get_all('Job Card',
|
||||
filters = {'work_order': work_order.name}, fields = ["operation_id", "name"])
|
||||
|
||||
if job_cards:
|
||||
job_card = job_cards[0]
|
||||
frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id)
|
||||
|
||||
doc = frappe.get_doc("Job Card", job_card.name)
|
||||
doc.operation_id = "Test Data"
|
||||
self.assertRaises(OperationMismatchError, doc.save)
|
||||
|
||||
def test_job_card_with_different_work_station(self):
|
||||
data = frappe.get_cached_value('BOM',
|
||||
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
|
||||
|
||||
if data:
|
||||
bom, bom_item = data
|
||||
|
||||
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
|
||||
|
||||
job_card = frappe.get_all('Job Card',
|
||||
filters = {'work_order': work_order.name},
|
||||
fields = ["operation_id", "workstation", "name", "for_quantity"])[0]
|
||||
|
||||
if job_card:
|
||||
workstation = frappe.db.get_value("Workstation",
|
||||
{"name": ("not in", [job_card.workstation])}, "name")
|
||||
|
||||
if not workstation or job_card.workstation == workstation:
|
||||
workstation = make_workstation(workstation_name=random_string(5)).name
|
||||
|
||||
doc = frappe.get_doc("Job Card", job_card.name)
|
||||
doc.workstation = workstation
|
||||
doc.append("time_logs", {
|
||||
"from_time": "2009-01-01 12:06:25",
|
||||
"to_time": "2009-01-01 12:37:25",
|
||||
"time_in_mins": "31.00002",
|
||||
"completed_qty": job_card.for_quantity
|
||||
})
|
||||
doc.submit()
|
||||
|
||||
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
|
||||
self.assertEqual(completed_qty, job_card.for_quantity)
|
||||
|
||||
doc.cancel()
|
||||
@@ -322,12 +322,13 @@ class ProductionPlan(Document):
|
||||
work_orders = []
|
||||
bom_data = {}
|
||||
|
||||
get_sub_assembly_items(item.get("bom_no"), bom_data)
|
||||
get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty"))
|
||||
|
||||
for key, data in bom_data.items():
|
||||
data.update({
|
||||
'qty': data.get("stock_qty") * item.get("qty"),
|
||||
'qty': data.get("stock_qty"),
|
||||
'production_plan': self.name,
|
||||
'use_multi_level_bom': item.get("use_multi_level_bom"),
|
||||
'company': self.company,
|
||||
'fg_warehouse': item.get("fg_warehouse"),
|
||||
'update_consumed_material_cost_in_project': 0
|
||||
@@ -724,7 +725,7 @@ def get_item_data(item_code):
|
||||
# "description": item_details.get("description")
|
||||
}
|
||||
|
||||
def get_sub_assembly_items(bom_no, bom_data):
|
||||
def get_sub_assembly_items(bom_no, bom_data, to_produce_qty):
|
||||
data = get_children('BOM', parent = bom_no)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
@@ -741,6 +742,6 @@ def get_sub_assembly_items(bom_no, bom_data):
|
||||
})
|
||||
|
||||
bom_item = bom_data.get(key)
|
||||
bom_item["stock_qty"] += d.stock_qty / d.parent_bom_qty
|
||||
bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
|
||||
|
||||
get_sub_assembly_items(bom_item.get("bom_no"), bom_data)
|
||||
get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"])
|
||||
|
||||
@@ -158,6 +158,46 @@ class TestProductionPlan(unittest.TestCase):
|
||||
self.assertTrue(mr.material_request_type, 'Customer Provided')
|
||||
self.assertTrue(mr.customer, '_Test Customer')
|
||||
|
||||
def test_production_plan_with_multi_level_bom(self):
|
||||
#|Item Code | Qty |
|
||||
#|Test BOM 1 | 1 |
|
||||
#| Test BOM 2 | 2 |
|
||||
#| Test BOM 3 | 3 |
|
||||
|
||||
for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]:
|
||||
create_item(item_code, is_stock_item=1)
|
||||
|
||||
# created bom upto 3 level
|
||||
if not frappe.db.get_value('BOM', {'item': "Test BOM 3"}):
|
||||
make_bom(item = "Test BOM 3", raw_materials = ["Test RM BOM 1"], rm_qty=3)
|
||||
|
||||
if not frappe.db.get_value('BOM', {'item': "Test BOM 2"}):
|
||||
make_bom(item = "Test BOM 2", raw_materials = ["Test BOM 3"], rm_qty=3)
|
||||
|
||||
if not frappe.db.get_value('BOM', {'item': "Test BOM 1"}):
|
||||
make_bom(item = "Test BOM 1", raw_materials = ["Test BOM 2"], rm_qty=2)
|
||||
|
||||
item_code = "Test BOM 1"
|
||||
pln = frappe.new_doc('Production Plan')
|
||||
pln.company = "_Test Company"
|
||||
pln.append("po_items", {
|
||||
"item_code": item_code,
|
||||
"bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
|
||||
"planned_qty": 3,
|
||||
"make_work_order_for_sub_assembly_items": 1
|
||||
})
|
||||
|
||||
pln.submit()
|
||||
pln.make_work_order()
|
||||
|
||||
#last level sub-assembly work order produce qty
|
||||
to_produce_qty = frappe.db.get_value("Work Order",
|
||||
{"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty")
|
||||
|
||||
self.assertEqual(to_produce_qty, 18.0)
|
||||
pln.cancel()
|
||||
frappe.delete_doc("Production Plan", pln.name)
|
||||
|
||||
def create_production_plan(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -205,7 +245,7 @@ def make_bom(**args):
|
||||
|
||||
bom.append('items', {
|
||||
'item_code': item,
|
||||
'qty': 1,
|
||||
'qty': args.rm_qty or 1.0,
|
||||
'uom': item_doc.stock_uom,
|
||||
'stock_uom': item_doc.stock_uom,
|
||||
'rate': item_doc.valuation_rate or args.rate,
|
||||
@@ -213,4 +253,4 @@ def make_bom(**args):
|
||||
|
||||
bom.insert(ignore_permissions=True)
|
||||
bom.submit()
|
||||
return bom
|
||||
return bom
|
||||
|
||||
@@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase):
|
||||
"_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00")
|
||||
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
|
||||
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
|
||||
|
||||
def make_workstation(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
try:
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Workstation",
|
||||
"workstation_name": args.workstation_name
|
||||
})
|
||||
|
||||
doc.insert()
|
||||
|
||||
return doc
|
||||
except frappe.DuplicateEntryError:
|
||||
return frappe.get_doc("Workstation", args.workstation_name)
|
||||
@@ -676,3 +676,5 @@ erpnext.patches.v12_0.move_due_advance_amount_to_pending_amount
|
||||
erpnext.patches.v12_0.set_multi_uom_in_rfq
|
||||
erpnext.patches.v12_0.update_state_code_for_daman_and_diu
|
||||
erpnext.patches.v12_0.rename_lost_reason_detail
|
||||
erpnext.patches.v12_0.update_leave_application_status
|
||||
erpnext.patches.v12_0.update_payment_entry_status
|
||||
10
erpnext/patches/v12_0/update_leave_application_status.py
Normal file
10
erpnext/patches/v12_0/update_leave_application_status.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
# frappe.reload_doc('HR', 'doctype', 'leave_application')
|
||||
frappe.db.sql("""
|
||||
UPDATE `tabLeave Application` SET
|
||||
status = 'Cancelled'
|
||||
WHERE status = 'Approved' and docstatus = 2
|
||||
""")
|
||||
7
erpnext/patches/v12_0/update_payment_entry_status.py
Normal file
7
erpnext/patches/v12_0/update_payment_entry_status.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("accounts", "doctype", "payment_entry")
|
||||
|
||||
frappe.db.sql(""" UPDATE `tabPayment Entry` set status = 'Cancelled' WHERE docstatus = 2 """)
|
||||
@@ -508,7 +508,7 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
|
||||
var d = frm.add_child("items");
|
||||
var item = r.message[i];
|
||||
for ( var key in item) {
|
||||
if ( !is_null(item[key]) ) {
|
||||
if ( !is_null(item[key]) && key !== "doctype" ) {
|
||||
d[key] = item[key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
this.calculate_total_advance(update_paid_amount);
|
||||
}
|
||||
|
||||
if (this.frm.doc.doctype == "Sales Invoice" && this.frm.doc.is_pos &&
|
||||
this.frm.doc.is_return) {
|
||||
this.update_paid_amount_for_return();
|
||||
}
|
||||
|
||||
// Sales person's commission
|
||||
if(in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"], this.frm.doc.doctype)) {
|
||||
this.calculate_commission();
|
||||
@@ -653,23 +658,58 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
||||
}
|
||||
},
|
||||
|
||||
set_default_payment: function(total_amount_to_pay, update_paid_amount){
|
||||
update_paid_amount_for_return: function() {
|
||||
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
||||
|
||||
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
|
||||
var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance
|
||||
- this.frm.doc.write_off_amount), precision("grand_total"));
|
||||
} else {
|
||||
var total_amount_to_pay = flt(
|
||||
(flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total"))
|
||||
- this.frm.doc.total_advance - this.frm.doc.base_write_off_amount),
|
||||
precision("base_grand_total")
|
||||
);
|
||||
}
|
||||
|
||||
frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1},
|
||||
['mode_of_payment', 'account', 'type'], (value) => {
|
||||
if (this.frm.is_dirty()) {
|
||||
frappe.model.clear_table(this.frm.doc, 'payments');
|
||||
if (value) {
|
||||
let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments');
|
||||
row.mode_of_payment = value.mode_of_payment;
|
||||
row.type = value.type;
|
||||
row.account = value.account;
|
||||
row.default = 1;
|
||||
row.amount = total_amount_to_pay;
|
||||
} else {
|
||||
this.frm.set_value('is_pos', 1);
|
||||
}
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
}, 'Sales Invoice');
|
||||
|
||||
this.calculate_paid_amount();
|
||||
},
|
||||
|
||||
set_default_payment: function(total_amount_to_pay, update_paid_amount) {
|
||||
var me = this;
|
||||
var payment_status = true;
|
||||
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)){
|
||||
$.each(this.frm.doc['payments'] || [], function(index, data){
|
||||
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
|
||||
$.each(this.frm.doc['payments'] || [], function(index, data) {
|
||||
if(data.default && payment_status && total_amount_to_pay > 0) {
|
||||
data.base_amount = flt(total_amount_to_pay, precision("base_amount"));
|
||||
data.amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount"));
|
||||
payment_status = false;
|
||||
}else if(me.frm.doc.paid_amount){
|
||||
} else if(me.frm.doc.paid_amount) {
|
||||
data.amount = 0.0;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
calculate_paid_amount: function(){
|
||||
calculate_paid_amount: function() {
|
||||
var me = this;
|
||||
var paid_amount = 0.0;
|
||||
var base_paid_amount = 0.0;
|
||||
|
||||
@@ -1789,7 +1789,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
},
|
||||
|
||||
set_query_for_item_tax_template: function(doc, cdt, cdn) {
|
||||
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if(!item.item_code) {
|
||||
frappe.throw(__("Please enter Item Code to get item taxes"));
|
||||
@@ -1797,7 +1796,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
let filters = {
|
||||
'item_code': item.item_code,
|
||||
'valid_from': doc.transaction_date || doc.bill_date || doc.posting_date,
|
||||
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
|
||||
'item_group': item.item_group,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="pos-list-row pos-bill-item {{ selected_class }}" data-item-code="{{ item_code }}">
|
||||
<div class="cell subject">
|
||||
<!--<input class="list-row-checkbox" type="checkbox" data-name="{{item_code}}">-->
|
||||
<a class="grey list-id" title="{{ item_name }}">{{ strip_html(__(item_name)) || item_code }}</a>
|
||||
<a class="grey list-id" title="{{ title }}">{{ strip_html(__(title)) }}</a>
|
||||
</div>
|
||||
<div class="cell text-right">{%= qty %}</div>
|
||||
<div class="cell text-right">{%= discount_percentage %}</div>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<div class="pos-item-wrapper image-view-item" data-item-code="{{item_code}}">
|
||||
<div class="image-view-header doclist-row">
|
||||
<div class="list-value">
|
||||
<a class="grey list-id" data-name="{{item_code}}" title="{{ item_name || item_code}}">{{item_name || item_code}}<br>({{ __(item_stock) }})</a>
|
||||
<a class="grey list-id" data-name="{{item_code}}" title="{{ title }}">{{ title }}<br>({{ __(item_stock) }})</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-view-body">
|
||||
<a data-item-code="{{ item_code }}"
|
||||
title="{{ item_name || item_code }}"
|
||||
title="{{ title }}"
|
||||
>
|
||||
<div class="image-field"
|
||||
style="
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
frappe.provide("erpnext.utils");
|
||||
|
||||
erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
if(!method) {
|
||||
if (!method) {
|
||||
method = "erpnext.accounts.party.get_party_details";
|
||||
}
|
||||
|
||||
@@ -22,12 +22,12 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
if(!args) {
|
||||
if((frm.doctype != "Purchase Order" && frm.doc.customer)
|
||||
if (!args) {
|
||||
if ((frm.doctype != "Purchase Order" && frm.doc.customer)
|
||||
|| (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) {
|
||||
|
||||
let party_type = "Customer";
|
||||
if(frm.doc.quotation_to && frm.doc.quotation_to === "Lead") {
|
||||
if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") {
|
||||
party_type = "Lead";
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
party_type: party_type,
|
||||
price_list: frm.doc.selling_price_list
|
||||
};
|
||||
} else if(frm.doc.supplier) {
|
||||
} else if (frm.doc.supplier) {
|
||||
args = {
|
||||
party: frm.doc.supplier,
|
||||
party_type: "Supplier",
|
||||
@@ -78,13 +78,17 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
args.posting_date = frm.doc.posting_date || frm.doc.transaction_date;
|
||||
}
|
||||
}
|
||||
if(!args || !args.party) return;
|
||||
if (!args || !args.party) return;
|
||||
|
||||
if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date",
|
||||
if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date",
|
||||
args.posting_date, args.party_type=="Customer" ? "customer": "supplier")) return;
|
||||
}
|
||||
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, args.party_type=="Customer" ? "customer": "supplier")) {
|
||||
return;
|
||||
}
|
||||
|
||||
args.currency = frm.doc.currency;
|
||||
args.company = frm.doc.company;
|
||||
args.doctype = frm.doc.doctype;
|
||||
@@ -92,14 +96,14 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
method: method,
|
||||
args: args,
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
if (r.message) {
|
||||
frm.supplier_tds = r.message.supplier_tds;
|
||||
frm.updating_party_details = true;
|
||||
frappe.run_serially([
|
||||
() => frm.set_value(r.message),
|
||||
() => {
|
||||
frm.updating_party_details = false;
|
||||
if(callback) callback();
|
||||
if (callback) callback();
|
||||
frm.refresh();
|
||||
erpnext.utils.add_item(frm);
|
||||
}
|
||||
@@ -110,9 +114,9 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
}
|
||||
|
||||
erpnext.utils.add_item = function(frm) {
|
||||
if(frm.is_new()) {
|
||||
if (frm.is_new()) {
|
||||
var prev_route = frappe.get_prev_route();
|
||||
if(prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) {
|
||||
if (prev_route[1]==='Item' && !(frm.doc.items && frm.doc.items.length)) {
|
||||
// add row
|
||||
var item = frm.add_child('items');
|
||||
frm.refresh_field('items');
|
||||
@@ -124,23 +128,23 @@ erpnext.utils.add_item = function(frm) {
|
||||
}
|
||||
|
||||
erpnext.utils.get_address_display = function(frm, address_field, display_field, is_your_company_address) {
|
||||
if(frm.updating_party_details) return;
|
||||
if (frm.updating_party_details) return;
|
||||
|
||||
if(!address_field) {
|
||||
if(frm.doctype != "Purchase Order" && frm.doc.customer) {
|
||||
if (!address_field) {
|
||||
if (frm.doctype != "Purchase Order" && frm.doc.customer) {
|
||||
address_field = "customer_address";
|
||||
} else if(frm.doc.supplier) {
|
||||
} else if (frm.doc.supplier) {
|
||||
address_field = "supplier_address";
|
||||
} else return;
|
||||
}
|
||||
|
||||
if(!display_field) display_field = "address_display";
|
||||
if(frm.doc[address_field]) {
|
||||
if (!display_field) display_field = "address_display";
|
||||
if (frm.doc[address_field]) {
|
||||
frappe.call({
|
||||
method: "frappe.contacts.doctype.address.address.get_address_display",
|
||||
args: {"address_dict": frm.doc[address_field] },
|
||||
callback: function(r) {
|
||||
if(r.message) {
|
||||
if (r.message) {
|
||||
frm.set_value(display_field, r.message)
|
||||
}
|
||||
}
|
||||
@@ -151,15 +155,15 @@ erpnext.utils.get_address_display = function(frm, address_field, display_field,
|
||||
};
|
||||
|
||||
erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billing_address_field, shipping_address_field) {
|
||||
if(frm.updating_party_details) return;
|
||||
if (frm.updating_party_details) return;
|
||||
|
||||
if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier",
|
||||
if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier",
|
||||
frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date",
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date",
|
||||
frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) {
|
||||
return;
|
||||
}
|
||||
@@ -175,8 +179,8 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi
|
||||
"shipping_address": frm.doc[shipping_address_field]
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc){
|
||||
if(frm.doc.tax_category != r.message) {
|
||||
if (!r.exc){
|
||||
if (frm.doc.tax_category != r.message) {
|
||||
frm.set_value("tax_category", r.message);
|
||||
} else {
|
||||
erpnext.utils.set_taxes(frm, triggered_from_field);
|
||||
@@ -187,13 +191,17 @@ erpnext.utils.set_taxes_from_address = function(frm, triggered_from_field, billi
|
||||
};
|
||||
|
||||
erpnext.utils.set_taxes = function(frm, triggered_from_field) {
|
||||
if(frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
if(!erpnext.utils.validate_mandatory(frm, "Lead/Customer/Supplier",
|
||||
if (frappe.meta.get_docfield(frm.doc.doctype, "taxes")) {
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Company", frm.doc.company, triggered_from_field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Lead / Customer / Supplier",
|
||||
frm.doc.customer || frm.doc.supplier || frm.doc.lead || frm.doc.party_name, triggered_from_field)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!erpnext.utils.validate_mandatory(frm, "Posting/Transaction Date",
|
||||
if (!erpnext.utils.validate_mandatory(frm, "Posting / Transaction Date",
|
||||
frm.doc.posting_date || frm.doc.transaction_date, triggered_from_field)) {
|
||||
return;
|
||||
}
|
||||
@@ -230,7 +238,7 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) {
|
||||
"shipping_address": frm.doc.shipping_address_name
|
||||
},
|
||||
callback: function(r) {
|
||||
if(r.message){
|
||||
if (r.message){
|
||||
frm.set_value("taxes_and_charges", r.message)
|
||||
}
|
||||
}
|
||||
@@ -238,14 +246,14 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) {
|
||||
};
|
||||
|
||||
erpnext.utils.get_contact_details = function(frm) {
|
||||
if(frm.updating_party_details) return;
|
||||
if (frm.updating_party_details) return;
|
||||
|
||||
if(frm.doc["contact_person"]) {
|
||||
if (frm.doc["contact_person"]) {
|
||||
frappe.call({
|
||||
method: "frappe.contacts.doctype.contact.contact.get_contact_details",
|
||||
args: {contact: frm.doc.contact_person },
|
||||
callback: function(r) {
|
||||
if(r.message)
|
||||
if (r.message)
|
||||
frm.set_value(r.message);
|
||||
}
|
||||
})
|
||||
@@ -253,10 +261,10 @@ erpnext.utils.get_contact_details = function(frm) {
|
||||
}
|
||||
|
||||
erpnext.utils.validate_mandatory = function(frm, label, value, trigger_on) {
|
||||
if(!value) {
|
||||
if (!value) {
|
||||
frm.doc[trigger_on] = "";
|
||||
refresh_field(trigger_on);
|
||||
frappe.msgprint(__("Please enter {0} first", [label]));
|
||||
frappe.throw({message:__("Please enter {0} first", [label]), title:__("Mandatory")});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -271,12 +279,12 @@ erpnext.utils.get_shipping_address = function(frm, callback){
|
||||
address: frm.doc.shipping_address
|
||||
},
|
||||
callback: function(r){
|
||||
if(r.message){
|
||||
if (r.message){
|
||||
frm.set_value("shipping_address", r.message[0]) //Address title or name
|
||||
frm.set_value("shipping_address_display", r.message[1]) //Address to be displayed on the page
|
||||
}
|
||||
|
||||
if(callback){
|
||||
if (callback){
|
||||
return callback();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = {
|
||||
],
|
||||
onload: (report) => {
|
||||
fetch_gstins(report);
|
||||
|
||||
report.page.add_inner_button(__("Download JSON"), function () {
|
||||
var filters = report.get_values();
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json',
|
||||
args: {
|
||||
data: report.data,
|
||||
report_name: report.report_name,
|
||||
filters: filters
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
const args = {
|
||||
cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file',
|
||||
data: r.message.data,
|
||||
report_name: r.message.report_name
|
||||
};
|
||||
open_url_post(frappe.request.url, args);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe, erpnext
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
from frappe.utils import flt, getdate, cstr
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils.xlsxutils import handle_html
|
||||
from six import iteritems
|
||||
import json
|
||||
from erpnext.regional.india.utils import get_gst_accounts
|
||||
from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number
|
||||
|
||||
def execute(filters=None):
|
||||
return _execute(filters)
|
||||
@@ -41,6 +43,12 @@ def _execute(filters=None):
|
||||
|
||||
data.append(row)
|
||||
added_item.append((d.parent, d.item_code))
|
||||
# gst is already added, just add qty and taxable value
|
||||
else:
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.base_net_amount, d.base_net_amount]
|
||||
for tax in tax_columns:
|
||||
row += [0]
|
||||
data.append(row)
|
||||
if data:
|
||||
data = get_merged_data(columns, data) # merge same hsn code data
|
||||
return columns, data
|
||||
@@ -141,7 +149,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
|
||||
|
||||
tax_details = frappe.db.sql("""
|
||||
select
|
||||
parent, description, item_wise_tax_detail,
|
||||
parent, account_head, item_wise_tax_detail,
|
||||
base_tax_amount_after_discount_amount
|
||||
from `tab%s`
|
||||
where
|
||||
@@ -153,11 +161,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
|
||||
""" % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions),
|
||||
tuple([doctype] + list(invoice_item_row)))
|
||||
|
||||
for parent, description, item_wise_tax_detail, tax_amount in tax_details:
|
||||
description = handle_html(description)
|
||||
if description not in tax_columns and tax_amount:
|
||||
for parent, account_head, item_wise_tax_detail, tax_amount in tax_details:
|
||||
|
||||
if account_head not in tax_columns and tax_amount:
|
||||
# as description is text editor earlier and markup can break the column convention in reports
|
||||
tax_columns.append(description)
|
||||
tax_columns.append(account_head)
|
||||
|
||||
if item_wise_tax_detail:
|
||||
try:
|
||||
@@ -175,17 +183,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
|
||||
for d in item_row_map.get(parent, {}).get(item_code, []):
|
||||
item_tax_amount = tax_amount
|
||||
if item_tax_amount:
|
||||
itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({
|
||||
itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({
|
||||
"tax_amount": flt(item_tax_amount, tax_amount_precision)
|
||||
})
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
tax_columns.sort()
|
||||
for desc in tax_columns:
|
||||
for account_head in tax_columns:
|
||||
columns.append({
|
||||
"label": desc,
|
||||
"fieldname": frappe.scrub(desc),
|
||||
"label": account_head,
|
||||
"fieldname": frappe.scrub(account_head),
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
})
|
||||
@@ -212,3 +220,76 @@ def get_merged_data(columns, data):
|
||||
|
||||
return result
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_json(filters, report_name, data):
|
||||
filters = json.loads(filters)
|
||||
report_data = json.loads(data)
|
||||
gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"])
|
||||
|
||||
if not filters.get('from_date') or not filters.get('to_date'):
|
||||
frappe.throw(_("Please enter From Date and To Date to generate JSON"))
|
||||
|
||||
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
|
||||
|
||||
gst_json = {"version": "GST2.3.4",
|
||||
"hash": "hash", "gstin": gstin, "fp": fp}
|
||||
|
||||
gst_json["hsn"] = {
|
||||
"data": get_hsn_wise_json_data(filters, report_data)
|
||||
}
|
||||
|
||||
return {
|
||||
'report_name': report_name,
|
||||
'data': gst_json
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_json_file():
|
||||
'''download json content in a file'''
|
||||
data = frappe._dict(frappe.local.form_dict)
|
||||
frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json'
|
||||
frappe.response['filecontent'] = data['data']
|
||||
frappe.response['content_type'] = 'application/json'
|
||||
frappe.response['type'] = 'download'
|
||||
|
||||
def get_hsn_wise_json_data(filters, report_data):
|
||||
|
||||
filters = frappe._dict(filters)
|
||||
gst_accounts = get_gst_accounts(filters.company)
|
||||
data = []
|
||||
count = 1
|
||||
|
||||
for hsn in report_data:
|
||||
row = {
|
||||
"num": count,
|
||||
"hsn_sc": hsn.get("gst_hsn_code"),
|
||||
"desc": hsn.get("description"),
|
||||
"uqc": hsn.get("stock_uom").upper(),
|
||||
"qty": hsn.get("stock_qty"),
|
||||
"val": flt(hsn.get("total_amount"), 2),
|
||||
"txval": flt(hsn.get("taxable_amount", 2)),
|
||||
"iamt": 0.0,
|
||||
"camt": 0.0,
|
||||
"samt": 0.0,
|
||||
"csamt": 0.0
|
||||
|
||||
}
|
||||
|
||||
for account in gst_accounts.get('igst_account'):
|
||||
row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
|
||||
|
||||
for account in gst_accounts.get('cgst_account'):
|
||||
row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
|
||||
|
||||
for account in gst_accounts.get('sgst_account'):
|
||||
row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
|
||||
|
||||
for account in gst_accounts.get('cess_account'):
|
||||
row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
|
||||
|
||||
data.append(row)
|
||||
count +=1
|
||||
|
||||
return data
|
||||
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ def execute(filters=None):
|
||||
if not filters:
|
||||
filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0])
|
||||
filters.setdefault('company', frappe.db.get_default("company"))
|
||||
|
||||
region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company })
|
||||
if region != 'United States':
|
||||
return [],[]
|
||||
|
||||
data = []
|
||||
columns = get_columns()
|
||||
data = frappe.db.sql("""
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase):
|
||||
|
||||
def test_irs_1099_report(self):
|
||||
make_payment_entry_to_irs_1099_supplier()
|
||||
filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"})
|
||||
filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"})
|
||||
columns, data = execute_1099_report(filters)
|
||||
print(columns, data)
|
||||
expected_row = {'supplier': '_US 1099 Test Supplier',
|
||||
@@ -42,10 +42,10 @@ def make_payment_entry_to_irs_1099_supplier():
|
||||
|
||||
pe = frappe.new_doc("Payment Entry")
|
||||
pe.payment_type = "Pay"
|
||||
pe.company = "_Test Company"
|
||||
pe.company = "_Test Company 1"
|
||||
pe.posting_date = "2016-01-10"
|
||||
pe.paid_from = "_Test Bank USD - _TC"
|
||||
pe.paid_to = "_Test Payable USD - _TC"
|
||||
pe.paid_from = "_Test Bank USD - _TC1"
|
||||
pe.paid_to = "_Test Payable USD - _TC1"
|
||||
pe.paid_amount = 100
|
||||
pe.received_amount = 100
|
||||
pe.reference_no = "For IRS 1099 testing"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:source_name",
|
||||
"beta": 0,
|
||||
"creation": "2016-09-16 01:47:47.382372",
|
||||
@@ -74,7 +74,7 @@
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-09-16 02:03:01.441622",
|
||||
"modified": "2020-09-16 02:03:01.441622",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Lead Source",
|
||||
@@ -128,4 +128,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -262,9 +262,17 @@ def _make_customer(source_name, ignore_permissions=False):
|
||||
return customer
|
||||
else:
|
||||
raise
|
||||
except frappe.MandatoryError:
|
||||
except frappe.MandatoryError as e:
|
||||
mandatory_fields = e.args[0].split(':')[1].split(',')
|
||||
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
|
||||
|
||||
frappe.local.message_log = []
|
||||
frappe.throw(_("Please create Customer from Lead {0}").format(lead_name))
|
||||
lead_link = frappe.utils.get_link_to_form("Lead", lead_name)
|
||||
message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
|
||||
message += "<br><ul><li>" + "</li><li>".join(mandatory_fields) + "</li></ul>"
|
||||
message += _("Please create Customer from Lead {0}.").format(lead_link)
|
||||
|
||||
frappe.throw(message, title=_("Mandatory Missing"))
|
||||
else:
|
||||
return customer_name
|
||||
else:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
||||
import frappe
|
||||
import json
|
||||
import frappe.utils
|
||||
from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form
|
||||
from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form, strip_html
|
||||
from frappe import _
|
||||
from six import string_types
|
||||
from frappe.model.utils import get_fetch_values
|
||||
@@ -994,15 +994,20 @@ def make_raw_material_request(items, company, sales_order, project=None):
|
||||
))
|
||||
for item in raw_materials:
|
||||
item_doc = frappe.get_cached_doc('Item', item.get('item_code'))
|
||||
|
||||
schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days))
|
||||
material_request.append('items', {
|
||||
'item_code': item.get('item_code'),
|
||||
'qty': item.get('quantity'),
|
||||
'schedule_date': schedule_date,
|
||||
'warehouse': item.get('warehouse'),
|
||||
'sales_order': sales_order,
|
||||
'project': project
|
||||
row = material_request.append('items', {
|
||||
'item_code': item.get('item_code'),
|
||||
'qty': item.get('quantity'),
|
||||
'schedule_date': schedule_date,
|
||||
'warehouse': item.get('warehouse'),
|
||||
'sales_order': sales_order,
|
||||
'project': project
|
||||
})
|
||||
|
||||
if not (strip_html(item.get("description")) and strip_html(item_doc.description)):
|
||||
row.description = item_doc.item_name or item.get('item_code')
|
||||
|
||||
material_request.insert()
|
||||
material_request.flags.ignore_permissions = 1
|
||||
material_request.run_method("set_missing_values")
|
||||
|
||||
@@ -416,8 +416,44 @@ class TestSalesOrder(unittest.TestCase):
|
||||
# add new item
|
||||
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}])
|
||||
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
|
||||
test_user.remove_roles("Accounts User")
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_update_child_qty_rate_with_workflow(self):
|
||||
from frappe.model.workflow import apply_workflow
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
workflow = make_sales_order_workflow()
|
||||
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
|
||||
apply_workflow(so, 'Approve')
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
user = 'test@example.com'
|
||||
test_user = frappe.get_doc('User', user)
|
||||
test_user.add_roles("Sales User", "Test Junior Approver")
|
||||
frappe.set_user(user)
|
||||
|
||||
# user shouldn't be able to edit since grand_total will become > 200 if qty is doubled
|
||||
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}])
|
||||
self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
user2 = 'test2@example.com'
|
||||
test_user2 = frappe.get_doc('User', user2)
|
||||
test_user2.add_roles("Sales User", "Test Approver")
|
||||
frappe.set_user(user2)
|
||||
|
||||
# Test Approver is allowed to edit with grand_total > 200
|
||||
update_child_qty_rate("Sales Order", trans_item, so.name)
|
||||
so.reload()
|
||||
self.assertEqual(so.items[0].qty, 2)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
test_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
|
||||
test_user2.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
|
||||
workflow.is_active = 0
|
||||
workflow.save()
|
||||
|
||||
def test_update_child_qty_rate_product_bundle(self):
|
||||
# test Update Items with product bundle
|
||||
if not frappe.db.exists("Item", "_Product Bundle Item"):
|
||||
@@ -953,3 +989,37 @@ def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"):
|
||||
"reserved_qty"))
|
||||
|
||||
test_dependencies = ["Currency Exchange"]
|
||||
|
||||
def make_sales_order_workflow():
|
||||
if frappe.db.exists('Workflow', 'SO Test Workflow'):
|
||||
doc = frappe.get_doc("Workflow", "SO Test Workflow")
|
||||
doc.set("is_active", 1)
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True)
|
||||
frappe.db.commit()
|
||||
frappe.cache().hdel('roles', frappe.session.user)
|
||||
|
||||
workflow = frappe.get_doc({
|
||||
"doctype": "Workflow",
|
||||
"workflow_name": "SO Test Workflow",
|
||||
"document_type": "Sales Order",
|
||||
"workflow_state_field": "workflow_state",
|
||||
"is_active": 1,
|
||||
"send_email_alert": 0,
|
||||
})
|
||||
workflow.append('states', dict( state = 'Pending', allow_edit = 'All' ))
|
||||
workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 ))
|
||||
workflow.append('transitions', dict(
|
||||
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1,
|
||||
condition = 'doc.grand_total < 200'
|
||||
))
|
||||
workflow.append('transitions', dict(
|
||||
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1,
|
||||
condition = 'doc.grand_total > 200'
|
||||
))
|
||||
workflow.insert(ignore_permissions=True)
|
||||
|
||||
return workflow
|
||||
@@ -286,7 +286,7 @@ erpnext.pos.PointOfSale = class PointOfSale {
|
||||
if (in_list(['serial_no', 'batch_no'], field)) {
|
||||
args[field] = value;
|
||||
}
|
||||
|
||||
|
||||
// add to cur_frm
|
||||
const item = this.frm.add_child('items', args);
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
@@ -436,7 +436,7 @@ erpnext.pos.PointOfSale = class PointOfSale {
|
||||
set_primary_action_in_modal() {
|
||||
if (!this.frm.msgbox) {
|
||||
this.frm.msgbox = frappe.msgprint(
|
||||
`<a class="btn btn-primary" onclick="cur_frm.print_preview.printit(true)" style="margin-right: 5px;">
|
||||
`<a class="btn btn-primary" style="margin-right: 5px;">
|
||||
${__('Print')}</a>
|
||||
<a class="btn btn-default">
|
||||
${__('New')}</a>`
|
||||
@@ -445,7 +445,15 @@ erpnext.pos.PointOfSale = class PointOfSale {
|
||||
$(this.frm.msgbox.body).find('.btn-default').on('click', () => {
|
||||
this.frm.msgbox.hide();
|
||||
this.make_new_invoice();
|
||||
})
|
||||
});
|
||||
|
||||
$(this.frm.msgbox.body).find('.btn-primary').on('click', () => {
|
||||
this.frm.msgbox.hide();
|
||||
const frm = this.events.get_frm();
|
||||
frm.doc = this.doc;
|
||||
frm.print_preview.lang_code = frm.doc.language;
|
||||
frm.print_preview.printit(true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,7 +688,10 @@ erpnext.pos.PointOfSale = class PointOfSale {
|
||||
if(this.frm.doc.docstatus != 1 ){
|
||||
await this.frm.save();
|
||||
}
|
||||
this.frm.print_preview.printit(true);
|
||||
const frm = this.events.get_frm();
|
||||
frm.doc = this.doc;
|
||||
frm.print_preview.lang_code = frm.doc.language;
|
||||
frm.print_preview.printit(true);
|
||||
});
|
||||
}
|
||||
if(this.frm.doc.items.length == 0){
|
||||
|
||||
@@ -241,3 +241,18 @@ class TestBatch(unittest.TestCase):
|
||||
batch.insert()
|
||||
|
||||
return batch
|
||||
|
||||
def make_new_batch(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
try:
|
||||
batch = frappe.get_doc({
|
||||
"doctype": "Batch",
|
||||
"batch_id": args.batch_id,
|
||||
"item": args.item_code,
|
||||
}).insert()
|
||||
|
||||
except frappe.DuplicateEntryError:
|
||||
batch = frappe.get_doc("Batch", args.batch_id)
|
||||
|
||||
return batch
|
||||
@@ -121,12 +121,18 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
|
||||
if (this.frm.doc.docstatus===0) {
|
||||
this.frm.add_custom_button(__('Sales Order'),
|
||||
function() {
|
||||
if (!me.frm.doc.customer) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Customer")
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
|
||||
source_doctype: "Sales Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
customer: me.frm.doc.customer || undefined,
|
||||
customer: me.frm.doc.customer,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
|
||||
@@ -13,6 +13,7 @@ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_ord
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry
|
||||
import unittest
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError
|
||||
|
||||
class TestItemAlternative(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -110,8 +111,11 @@ def make_items():
|
||||
if not frappe.db.exists('Item', item_code):
|
||||
create_item(item_code)
|
||||
|
||||
create_stock_reconciliation(item_code="Test FG A RW 1",
|
||||
warehouse='_Test Warehouse - _TC', qty=10, rate=2000)
|
||||
try:
|
||||
create_stock_reconciliation(item_code="Test FG A RW 1",
|
||||
warehouse='_Test Warehouse - _TC', qty=10, rate=2000)
|
||||
except EmptyStockReconciliationItemsError:
|
||||
pass
|
||||
|
||||
if frappe.db.exists('Item', 'Test FG A RW 1'):
|
||||
doc = frappe.get_doc('Item', 'Test FG A RW 1')
|
||||
|
||||
@@ -101,12 +101,18 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
|
||||
if (this.frm.doc.docstatus == 0) {
|
||||
this.frm.add_custom_button(__('Purchase Order'),
|
||||
function () {
|
||||
if (!me.frm.doc.supplier) {
|
||||
frappe.throw({
|
||||
title: __("Mandatory"),
|
||||
message: __("Please Select a Supplier")
|
||||
});
|
||||
}
|
||||
erpnext.utils.map_current_doc({
|
||||
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
|
||||
source_doctype: "Purchase Order",
|
||||
target: me.frm,
|
||||
setters: {
|
||||
supplier: me.frm.doc.supplier || undefined,
|
||||
supplier: me.frm.doc.supplier,
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
|
||||
@@ -223,6 +223,15 @@ class PurchaseReceipt(BuyingController):
|
||||
|
||||
if not stock_value_diff:
|
||||
continue
|
||||
|
||||
# If PR is sub-contracted and fg item rate is zero
|
||||
# in that case if account for shource and target warehouse are same,
|
||||
# then GL entries should not be posted
|
||||
if flt(stock_value_diff) == flt(d.rm_supp_cost) \
|
||||
and warehouse_account.get(self.supplier_warehouse) \
|
||||
and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]:
|
||||
continue
|
||||
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
"account": warehouse_account[d.warehouse]["account"],
|
||||
"against": stock_rbnb,
|
||||
@@ -232,16 +241,17 @@ class PurchaseReceipt(BuyingController):
|
||||
}, warehouse_account[d.warehouse]["account_currency"], item=d))
|
||||
|
||||
# stock received but not billed
|
||||
stock_rbnb_currency = get_account_currency(stock_rbnb)
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
"account": stock_rbnb,
|
||||
"against": warehouse_account[d.warehouse]["account"],
|
||||
"cost_center": d.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(d.base_net_amount, d.precision("base_net_amount")),
|
||||
"credit_in_account_currency": flt(d.base_net_amount, d.precision("base_net_amount")) \
|
||||
if stock_rbnb_currency==self.company_currency else flt(d.net_amount, d.precision("net_amount"))
|
||||
}, stock_rbnb_currency, item=d))
|
||||
if d.base_net_amount:
|
||||
stock_rbnb_currency = get_account_currency(stock_rbnb)
|
||||
gl_entries.append(self.get_gl_dict({
|
||||
"account": stock_rbnb,
|
||||
"against": warehouse_account[d.warehouse]["account"],
|
||||
"cost_center": d.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(d.base_net_amount, d.precision("base_net_amount")),
|
||||
"credit_in_account_currency": flt(d.base_net_amount, d.precision("base_net_amount")) \
|
||||
if stock_rbnb_currency==self.company_currency else flt(d.net_amount, d.precision("net_amount"))
|
||||
}, stock_rbnb_currency, item=d))
|
||||
|
||||
negative_expense_to_be_booked += flt(d.item_tax_amount)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import unittest
|
||||
import json
|
||||
import frappe, erpnext
|
||||
import frappe.defaults
|
||||
from frappe.utils import cint, flt, cstr, today, random_string
|
||||
@@ -121,6 +122,87 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
|
||||
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
|
||||
|
||||
def test_subcontracting_gle_fg_item_rate_zero(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
set_perpetual_inventory()
|
||||
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
|
||||
make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
|
||||
make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
|
||||
qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
|
||||
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
|
||||
company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1')
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
|
||||
|
||||
self.assertFalse(gl_entries)
|
||||
|
||||
set_perpetual_inventory(0)
|
||||
|
||||
def test_subcontracting_over_receipt(self):
|
||||
"""
|
||||
Behaviour: Raise multiple PRs against one PO that in total
|
||||
receive more than the required qty in the PO.
|
||||
Expected Result: Error Raised for Over Receipt against PO.
|
||||
"""
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on,
|
||||
make_subcontracted_item, create_purchase_order)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (make_purchase_receipt,
|
||||
make_rm_stock_entry as make_subcontract_transfer_entry)
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=1,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
#stock raw materials in a warehouse before transfer
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code="_Test Item Home Desktop 100", qty=1, basic_rate=100)
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Test Extra Item 1", qty=1, basic_rate=100)
|
||||
make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "_Test Item", qty=1, basic_rate=100)
|
||||
|
||||
rm_items = [
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": po.supplied_items[0].rm_item_code,
|
||||
"item_name": "_Test Item",
|
||||
"qty": po.supplied_items[0].required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos"
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": po.supplied_items[1].rm_item_code,
|
||||
"item_name": "Test Extra Item 1",
|
||||
"qty": po.supplied_items[1].required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos"
|
||||
},
|
||||
{
|
||||
"item_code": item_code,
|
||||
"rm_item_code": po.supplied_items[2].rm_item_code,
|
||||
"item_name": "_Test Item Home Desktop 100",
|
||||
"qty": po.supplied_items[2].required_qty,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"stock_uom": "Nos"
|
||||
}
|
||||
]
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||
se.to_warehouse = "_Test Warehouse 1 - _TC"
|
||||
se.save()
|
||||
se.submit()
|
||||
|
||||
pr1 = make_purchase_receipt(po.name)
|
||||
pr2 = make_purchase_receipt(po.name)
|
||||
|
||||
pr1.submit()
|
||||
self.assertRaises(frappe.ValidationError, pr2.submit)
|
||||
|
||||
def test_serial_no_supplier(self):
|
||||
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
|
||||
@@ -497,6 +579,67 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
self.assertEquals(pi2.items[0].qty, 2)
|
||||
self.assertEquals(pi2.items[1].qty, 1)
|
||||
|
||||
def test_subcontracted_pr_for_multi_transfer_batches(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry, make_purchase_receipt
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on,
|
||||
create_purchase_order)
|
||||
|
||||
update_backflush_based_on("Material Transferred for Subcontract")
|
||||
item_code = "_Test Subcontracted FG Item 3"
|
||||
|
||||
make_item('Sub Contracted Raw Material 3', {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1,
|
||||
'has_batch_no': 1,
|
||||
'create_new_batch': 1
|
||||
})
|
||||
|
||||
create_subcontracted_item(item_code=item_code, has_batch_no=1,
|
||||
raw_materials=["Sub Contracted Raw Material 3"])
|
||||
|
||||
order_qty = 500
|
||||
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
ste1=make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100)
|
||||
ste2=make_stock_entry(target="_Test Warehouse - _TC",
|
||||
item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100)
|
||||
|
||||
transferred_batch = {
|
||||
ste1.items[0].batch_no : 300,
|
||||
ste2.items[0].batch_no : 200
|
||||
}
|
||||
|
||||
rm_items = [
|
||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
|
||||
"qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
|
||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
|
||||
"qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}
|
||||
]
|
||||
|
||||
rm_item_string = json.dumps(rm_items)
|
||||
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
|
||||
self.assertEqual(len(se.items), 2)
|
||||
se.items[0].batch_no = ste1.items[0].batch_no
|
||||
se.items[1].batch_no = ste2.items[0].batch_no
|
||||
se.submit()
|
||||
|
||||
supplied_qty = frappe.db.get_value("Purchase Order Item Supplied",
|
||||
{"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty")
|
||||
|
||||
self.assertEqual(supplied_qty, 500.00)
|
||||
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.save()
|
||||
self.assertEqual(len(pr.supplied_items), 2)
|
||||
|
||||
for row in pr.supplied_items:
|
||||
self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty)
|
||||
|
||||
update_backflush_based_on("BOM")
|
||||
|
||||
def get_gl_entries(voucher_type, voucher_no):
|
||||
return frappe.db.sql("""select account, debit, credit, cost_center
|
||||
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
|
||||
@@ -607,7 +750,7 @@ def make_purchase_receipt(**args):
|
||||
"received_qty": received_qty,
|
||||
"rejected_qty": rejected_qty,
|
||||
"rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "",
|
||||
"rate": args.rate or 50,
|
||||
"rate": args.rate if args.rate != None else 50,
|
||||
"conversion_factor": args.conversion_factor or 1.0,
|
||||
"serial_no": args.serial_no,
|
||||
"stock_uom": args.stock_uom or "_Test UOM",
|
||||
@@ -632,6 +775,33 @@ def make_purchase_receipt(**args):
|
||||
pr.submit()
|
||||
return pr
|
||||
|
||||
def create_subcontracted_item(**args):
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
args = frappe._dict(args)
|
||||
|
||||
if not frappe.db.exists('Item', args.item_code):
|
||||
make_item(args.item_code, {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1,
|
||||
'has_batch_no': args.get("has_batch_no") or 0
|
||||
})
|
||||
|
||||
if not args.raw_materials:
|
||||
if not frappe.db.exists('Item', "Test Extra Item 1"):
|
||||
make_item("Test Extra Item 1", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
|
||||
if not frappe.db.exists('Item', "Test Extra Item 2"):
|
||||
make_item("Test Extra Item 2", {
|
||||
'is_stock_item': 1,
|
||||
})
|
||||
|
||||
args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
|
||||
|
||||
if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'):
|
||||
make_bom(item = args.item_code, raw_materials = args.get("raw_materials"))
|
||||
|
||||
test_dependencies = ["BOM", "Item Price", "Location"]
|
||||
test_records = frappe.get_test_records('Purchase Receipt')
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
"fieldname": "reference_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry"
|
||||
"options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
@@ -83,7 +84,8 @@
|
||||
"label": "Reference Name",
|
||||
"oldfieldname": "purchase_receipt_no",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "reference_type"
|
||||
"options": "reference_type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
@@ -230,8 +232,10 @@
|
||||
],
|
||||
"icon": "fa fa-search",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"modified": "2019-07-12 12:07:23.153698",
|
||||
"links": [],
|
||||
"modified": "2020-09-12 16:11:31.910508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Quality Inspection",
|
||||
|
||||
@@ -420,6 +420,9 @@ def get_item_details(item_code):
|
||||
from tabItem where name=%s""", item_code, as_dict=True)[0]
|
||||
|
||||
def get_serial_nos(serial_no):
|
||||
if isinstance(serial_no, list):
|
||||
return serial_no
|
||||
|
||||
return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
|
||||
if s.strip()]
|
||||
|
||||
|
||||
@@ -500,7 +500,7 @@ class StockEntry(StockController):
|
||||
d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
|
||||
elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
|
||||
d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
|
||||
d.basic_amount = d.basic_rate * d.qty
|
||||
d.basic_amount = d.basic_rate * flt(d.qty)
|
||||
|
||||
def distribute_additional_costs(self):
|
||||
if self.purpose == "Material Issue":
|
||||
@@ -556,8 +556,9 @@ class StockEntry(StockController):
|
||||
qty_allowance = flt(frappe.db.get_single_value("Buying Settings",
|
||||
"over_transfer_allowance"))
|
||||
|
||||
if (self.purpose == "Send to Subcontractor" and self.purchase_order and
|
||||
backflush_raw_materials_based_on == 'BOM'):
|
||||
if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return
|
||||
|
||||
if (backflush_raw_materials_based_on == 'BOM'):
|
||||
purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
|
||||
for se_item in self.items:
|
||||
item_code = se_item.original_item or se_item.item_code
|
||||
@@ -594,6 +595,11 @@ class StockEntry(StockController):
|
||||
if flt(total_supplied, precision) > flt(total_allowed, precision):
|
||||
frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}")
|
||||
.format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order))
|
||||
elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
|
||||
for row in self.items:
|
||||
if not row.subcontracted_item:
|
||||
frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}")
|
||||
.format(row.idx, frappe.bold(row.item_code)))
|
||||
|
||||
def validate_bom(self):
|
||||
for d in self.get('items'):
|
||||
@@ -797,6 +803,13 @@ class StockEntry(StockController):
|
||||
ret.get('has_batch_no') and not args.get('batch_no')):
|
||||
args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty'])
|
||||
|
||||
if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'):
|
||||
subcontract_items = frappe.get_all("Purchase Order Item Supplied",
|
||||
{"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code")
|
||||
|
||||
if subcontract_items and len(subcontract_items) == 1:
|
||||
ret["subcontracted_item"] = subcontract_items[0].main_item_code
|
||||
|
||||
return ret
|
||||
|
||||
def set_items_for_stock_in(self):
|
||||
@@ -1237,9 +1250,15 @@ class StockEntry(StockController):
|
||||
#Update Supplied Qty in PO Supplied Items
|
||||
|
||||
frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos
|
||||
SET pos.supplied_qty = (SELECT ifnull(sum(transfer_qty), 0) FROM `tabStock Entry Detail` sed
|
||||
WHERE pos.name = sed.po_detail and sed.docstatus = 1)
|
||||
WHERE pos.docstatus = 1 and pos.parent = %s""", self.purchase_order)
|
||||
SET
|
||||
pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0)
|
||||
FROM
|
||||
`tabStock Entry Detail` sed, `tabStock Entry` se
|
||||
WHERE
|
||||
(pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code)
|
||||
AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s
|
||||
), 0)
|
||||
WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
|
||||
|
||||
#Update reserved sub contracted quantity in bin based on Supplied Item Details and
|
||||
for d in self.get("items"):
|
||||
|
||||
@@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry
|
||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from six import iteritems
|
||||
|
||||
def get_sle(**args):
|
||||
@@ -483,6 +484,100 @@ class TestStockEntry(unittest.TestCase):
|
||||
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
|
||||
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
|
||||
|
||||
def test_serial_batch_item_stock_entry(self):
|
||||
"""
|
||||
Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
|
||||
2) Cancel same Stock Entry
|
||||
Expected Result: 1) Batch is created with Reference in Serial No
|
||||
2) Batch is deleted and Serial No is Inactive
|
||||
"""
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
if not item:
|
||||
item = create_item("Batched and Serialised Item")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "B-BATCH-.##"
|
||||
item.serial_no_series = "S-.####"
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
|
||||
se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
|
||||
batch_no = se.items[0].batch_no
|
||||
serial_no = get_serial_nos(se.items[0].serial_no)[0]
|
||||
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||
|
||||
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
|
||||
self.assertEqual(batch_in_serial_no, batch_no)
|
||||
|
||||
self.assertEqual(batch_qty, 1)
|
||||
|
||||
se.cancel()
|
||||
|
||||
batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
|
||||
self.assertEqual(batch_in_serial_no, None)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
|
||||
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||
|
||||
def test_serial_batch_item_qty_deduction(self):
|
||||
"""
|
||||
Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch
|
||||
Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch)
|
||||
should throw a LinkExistsError
|
||||
2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch
|
||||
and in that transaction only, Inactive.
|
||||
"""
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
if not item:
|
||||
item = create_item("Batched and Serialised Item")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "B-BATCH-.##"
|
||||
item.serial_no_series = "S-.####"
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
|
||||
se1 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
|
||||
batch_no = se1.items[0].batch_no
|
||||
serial_no1 = get_serial_nos(se1.items[0].serial_no)[0]
|
||||
|
||||
# Check Source (Origin) Document of Batch
|
||||
self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name)
|
||||
|
||||
se2 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100,
|
||||
batch_no=batch_no)
|
||||
serial_no2 = get_serial_nos(se2.items[0].serial_no)[0]
|
||||
|
||||
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||
self.assertEqual(batch_qty, 2)
|
||||
frappe.db.commit()
|
||||
|
||||
# Cancelling Origin Document of Batch
|
||||
self.assertRaises(frappe.LinkExistsError, se1.cancel)
|
||||
frappe.db.rollback()
|
||||
|
||||
se2.cancel()
|
||||
|
||||
# Check decrease in Batch Qty
|
||||
batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
|
||||
self.assertEqual(batch_qty, 1)
|
||||
|
||||
# Check if Serial No from Stock Entry 1 is intact
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active")
|
||||
|
||||
# Check if Serial No from Stock Entry 2 is Unlinked and Inactive
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
|
||||
|
||||
def test_warehouse_company_validation(self):
|
||||
company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
|
||||
set_perpetual_inventory(0, company)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-03-29 18:22:12",
|
||||
"doctype": "DocType",
|
||||
@@ -17,6 +16,7 @@
|
||||
"item_group",
|
||||
"col_break2",
|
||||
"item_name",
|
||||
"subcontracted_item",
|
||||
"section_break_8",
|
||||
"description",
|
||||
"column_break_10",
|
||||
@@ -57,7 +57,6 @@
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"original_item",
|
||||
"subcontracted_item",
|
||||
"reference_section",
|
||||
"against_stock_entry",
|
||||
"ste_detail",
|
||||
@@ -415,6 +414,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.purpose == 'Send to Subcontractor'",
|
||||
"fieldname": "subcontracted_item",
|
||||
"fieldtype": "Link",
|
||||
"label": "Subcontracted Item",
|
||||
@@ -497,15 +497,12 @@
|
||||
"depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse",
|
||||
"fieldname": "set_basic_rate_manually",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Basic Rate Manually",
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
"label": "Set Basic Rate Manually"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-06-08 12:57:03.172887",
|
||||
"modified": "2020-09-04 12:12:35.668198",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Entry Detail",
|
||||
|
||||
@@ -45,6 +45,7 @@ class StockReconciliation(StockController):
|
||||
def on_cancel(self):
|
||||
self.delete_and_repost_sle()
|
||||
self.make_gl_entries_on_cancel()
|
||||
self.delete_auto_created_batches()
|
||||
|
||||
def remove_items_with_no_change(self):
|
||||
"""Remove items if qty or rate is not changed"""
|
||||
@@ -183,17 +184,11 @@ class StockReconciliation(StockController):
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
|
||||
sl_entries = []
|
||||
has_serial_no = False
|
||||
has_batch_no = False
|
||||
for row in self.items:
|
||||
item = frappe.get_doc("Item", row.item_code)
|
||||
if item.has_batch_no:
|
||||
has_batch_no = True
|
||||
|
||||
if item.has_serial_no or item.has_batch_no:
|
||||
has_serial_no = True
|
||||
self.get_sle_for_serialized_items(row, sl_entries)
|
||||
else:
|
||||
serialized_items = False
|
||||
for row in self.items:
|
||||
item = frappe.get_cached_doc("Item", row.item_code)
|
||||
if not (item.has_serial_no or item.has_batch_no):
|
||||
if row.serial_no or row.batch_no:
|
||||
frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
|
||||
.format(row.idx, frappe.bold(row.item_code)))
|
||||
@@ -221,88 +216,91 @@ class StockReconciliation(StockController):
|
||||
|
||||
sl_entries.append(self.get_sle_for_items(row))
|
||||
|
||||
else:
|
||||
serialized_items = True
|
||||
|
||||
if serialized_items:
|
||||
self.get_sle_for_serialized_items(sl_entries)
|
||||
|
||||
if sl_entries:
|
||||
if has_serial_no:
|
||||
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
||||
|
||||
allow_negative_stock = False
|
||||
if has_batch_no:
|
||||
allow_negative_stock = True
|
||||
|
||||
allow_negative_stock = frappe.get_cached_value("Stock Settings", None, "allow_negative_stock")
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
||||
|
||||
if has_serial_no and sl_entries:
|
||||
self.update_valuation_rate_for_serial_no()
|
||||
def get_sle_for_serialized_items(self, sl_entries):
|
||||
self.issue_existing_serial_and_batch(sl_entries)
|
||||
self.add_new_serial_and_batch(sl_entries)
|
||||
self.update_valuation_rate_for_serial_no()
|
||||
|
||||
def get_sle_for_serialized_items(self, row, sl_entries):
|
||||
if sl_entries:
|
||||
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
||||
|
||||
def issue_existing_serial_and_batch(self, sl_entries):
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
|
||||
serial_nos = get_serial_nos(row.serial_no)
|
||||
for row in self.items:
|
||||
serial_nos = get_serial_nos(row.serial_no) or []
|
||||
|
||||
|
||||
# To issue existing serial nos
|
||||
if row.current_qty and (row.current_serial_no or row.batch_no):
|
||||
args = self.get_sle_for_items(row)
|
||||
args.update({
|
||||
'actual_qty': -1 * row.current_qty,
|
||||
'serial_no': row.current_serial_no,
|
||||
'batch_no': row.batch_no,
|
||||
'valuation_rate': row.current_valuation_rate
|
||||
})
|
||||
|
||||
if row.current_serial_no:
|
||||
# To issue existing serial nos
|
||||
if row.current_qty and (row.current_serial_no or row.batch_no):
|
||||
args = self.get_sle_for_items(row)
|
||||
args.update({
|
||||
'qty_after_transaction': 0,
|
||||
'actual_qty': -1 * row.current_qty,
|
||||
'serial_no': row.current_serial_no,
|
||||
'batch_no': row.batch_no,
|
||||
'valuation_rate': row.current_valuation_rate
|
||||
})
|
||||
|
||||
sl_entries.append(args)
|
||||
if row.current_serial_no:
|
||||
args.update({
|
||||
'qty_after_transaction': 0,
|
||||
})
|
||||
|
||||
qty_after_transaction = 0
|
||||
for serial_no in serial_nos:
|
||||
args = self.get_sle_for_items(row, [serial_no])
|
||||
sl_entries.append(args)
|
||||
|
||||
previous_sle = get_previous_sle({
|
||||
"item_code": row.item_code,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"serial_no": serial_no
|
||||
})
|
||||
qty_after_transaction = 0
|
||||
for serial_no in serial_nos:
|
||||
args = self.get_sle_for_items(row, [serial_no])
|
||||
|
||||
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
|
||||
# If serial no exists in different warehouse
|
||||
|
||||
warehouse = previous_sle.get("warehouse", '') or row.warehouse
|
||||
|
||||
if not qty_after_transaction:
|
||||
qty_after_transaction = get_stock_balance(row.item_code,
|
||||
warehouse, self.posting_date, self.posting_time)
|
||||
|
||||
qty_after_transaction -= 1
|
||||
|
||||
new_args = args.copy()
|
||||
new_args.update({
|
||||
'actual_qty': -1,
|
||||
'qty_after_transaction': qty_after_transaction,
|
||||
'warehouse': warehouse,
|
||||
'valuation_rate': previous_sle.get("valuation_rate")
|
||||
previous_sle = get_previous_sle({
|
||||
"item_code": row.item_code,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"serial_no": serial_no
|
||||
})
|
||||
|
||||
sl_entries.append(new_args)
|
||||
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
|
||||
# If serial no exists in different warehouse
|
||||
|
||||
if row.qty:
|
||||
args = self.get_sle_for_items(row)
|
||||
warehouse = previous_sle.get("warehouse", '') or row.warehouse
|
||||
|
||||
args.update({
|
||||
'actual_qty': row.qty,
|
||||
'incoming_rate': row.valuation_rate,
|
||||
'valuation_rate': row.valuation_rate
|
||||
})
|
||||
if not qty_after_transaction:
|
||||
qty_after_transaction = get_stock_balance(row.item_code,
|
||||
warehouse, self.posting_date, self.posting_time)
|
||||
|
||||
sl_entries.append(args)
|
||||
qty_after_transaction -= 1
|
||||
|
||||
if serial_nos == get_serial_nos(row.current_serial_no):
|
||||
# update valuation rate
|
||||
self.update_valuation_rate_for_serial_nos(row, serial_nos)
|
||||
new_args = args.copy()
|
||||
new_args.update({
|
||||
'actual_qty': -1,
|
||||
'qty_after_transaction': qty_after_transaction,
|
||||
'warehouse': warehouse,
|
||||
'valuation_rate': previous_sle.get("valuation_rate")
|
||||
})
|
||||
|
||||
sl_entries.append(new_args)
|
||||
|
||||
def add_new_serial_and_batch(self, sl_entries):
|
||||
for row in self.items:
|
||||
if row.qty:
|
||||
args = self.get_sle_for_items(row)
|
||||
|
||||
args.update({
|
||||
'actual_qty': row.qty,
|
||||
'incoming_rate': row.valuation_rate,
|
||||
'valuation_rate': row.valuation_rate
|
||||
})
|
||||
|
||||
sl_entries.append(args)
|
||||
|
||||
def update_valuation_rate_for_serial_no(self):
|
||||
for d in self.items:
|
||||
@@ -360,17 +358,9 @@ class StockReconciliation(StockController):
|
||||
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
|
||||
|
||||
sl_entries = []
|
||||
|
||||
has_serial_no = False
|
||||
for row in self.items:
|
||||
if row.serial_no or row.batch_no or row.current_serial_no:
|
||||
has_serial_no = True
|
||||
self.get_sle_for_serialized_items(row, sl_entries)
|
||||
self.get_sle_for_serialized_items(sl_entries)
|
||||
|
||||
if sl_entries:
|
||||
if has_serial_no:
|
||||
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
||||
|
||||
sl_entries.reverse()
|
||||
allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
|
||||
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
||||
|
||||
@@ -170,7 +170,7 @@ class TestStockReconciliation(unittest.TestCase):
|
||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(item_code=item_code,
|
||||
warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
|
||||
warehouse = warehouse, qty=5, rate=200, do_not_save=1, do_not_submit=1)
|
||||
sr.save(ignore_permissions=True)
|
||||
sr.submit()
|
||||
|
||||
@@ -204,6 +204,162 @@ class TestStockReconciliation(unittest.TestCase):
|
||||
stock_doc = frappe.get_doc("Stock Reconciliation", d)
|
||||
stock_doc.cancel()
|
||||
|
||||
def test_stock_reco_for_serial_and_batch_item(self):
|
||||
set_perpetual_inventory()
|
||||
|
||||
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
if not item:
|
||||
item = create_item("Batched and Serialised Item")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "B-BATCH-.##"
|
||||
item.serial_no_series = "S-.####"
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
|
||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||
|
||||
sr = create_stock_reconciliation(item_code=item.item_code,
|
||||
warehouse = warehouse, qty=1, rate=100)
|
||||
|
||||
batch_no = sr.items[0].batch_no
|
||||
|
||||
serial_nos = get_serial_nos(sr.items[0].serial_no)
|
||||
self.assertEqual(len(serial_nos), 1)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no)
|
||||
|
||||
sr.cancel()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
|
||||
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||
|
||||
if frappe.db.exists("Serial No", serial_nos[0]):
|
||||
frappe.delete_doc("Serial No", serial_nos[0])
|
||||
|
||||
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
|
||||
"""
|
||||
Behaviour: 1) Create Stock Reconciliation, which will be the origin document
|
||||
of a new batch having a serial no
|
||||
2) Create a Stock Entry that adds a serial no to the same batch following this
|
||||
Stock Reconciliation
|
||||
3) Cancel Stock Reconciliation
|
||||
4) Cancel Stock Entry
|
||||
Expected Result: 3) Cancelling the Stock Reco throws a LinkExistsError since
|
||||
Stock Entry is dependent on the batch involved
|
||||
4) Serial No only in the Stock Entry is Inactive and Batch qty decreases
|
||||
"""
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
|
||||
set_perpetual_inventory()
|
||||
|
||||
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
if not item:
|
||||
item = create_item("Batched and Serialised Item")
|
||||
item.has_batch_no = 1
|
||||
item.create_new_batch = 1
|
||||
item.has_serial_no = 1
|
||||
item.batch_number_series = "B-BATCH-.##"
|
||||
item.serial_no_series = "S-.####"
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
|
||||
|
||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||
|
||||
stock_reco = create_stock_reconciliation(item_code=item.item_code,
|
||||
warehouse = warehouse, qty=1, rate=100)
|
||||
batch_no = stock_reco.items[0].batch_no
|
||||
serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0]
|
||||
|
||||
stock_entry = make_stock_entry(item_code=item.item_code, target=warehouse, qty=1, basic_rate=100,
|
||||
batch_no=batch_no)
|
||||
serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0]
|
||||
|
||||
# Check Batch qty after 2 transactions
|
||||
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
|
||||
self.assertEqual(batch_qty, 2)
|
||||
frappe.db.commit()
|
||||
|
||||
# Cancelling Origin Document of Batch
|
||||
self.assertRaises(frappe.LinkExistsError, stock_reco.cancel)
|
||||
frappe.db.rollback()
|
||||
|
||||
stock_entry.cancel()
|
||||
|
||||
# Check Batch qty after cancellation
|
||||
batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
|
||||
self.assertEqual(batch_qty, 1)
|
||||
|
||||
# Check if Serial No from Stock Reconcilation is intact
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "batch_no"), batch_no)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active")
|
||||
|
||||
# Check if Serial No from Stock Entry is Unlinked and Inactive
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive")
|
||||
|
||||
stock_reco.load_from_db()
|
||||
stock_reco.cancel()
|
||||
|
||||
for sn in (serial_no, serial_no_2):
|
||||
if frappe.db.exists("Serial No", sn):
|
||||
frappe.delete_doc("Serial No", sn)
|
||||
|
||||
def test_stock_reco_for_same_item_with_multiple_batches(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
set_perpetual_inventory()
|
||||
|
||||
item_code = "Stock-Reco-batch-Item-2"
|
||||
warehouse = "_Test Warehouse for Stock Reco3 - _TC"
|
||||
|
||||
create_warehouse("_Test Warehouse for Stock Reco3", {"is_group": 0,
|
||||
"parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"})
|
||||
|
||||
batch_item_doc = create_item(item_code, is_stock_item=1)
|
||||
if not batch_item_doc.has_batch_no:
|
||||
frappe.db.set_value("Item", item_code, {
|
||||
"has_batch_no": 1,
|
||||
"create_new_batch": 1,
|
||||
"batch_number_series": "Test-C.####"
|
||||
})
|
||||
|
||||
# inward entries with different batch and valuation rate
|
||||
ste1=make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code=item_code,
|
||||
target=warehouse, qty=6, basic_rate=700)
|
||||
ste2=make_stock_entry(posting_date="2012-12-16", posting_time="02:00", item_code=item_code,
|
||||
target=warehouse, qty=3, basic_rate=200)
|
||||
ste3=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code,
|
||||
target=warehouse, qty=2, basic_rate=500)
|
||||
ste4=make_stock_entry(posting_date="2012-12-17", posting_time="02:00", item_code=item_code,
|
||||
target=warehouse, qty=4, basic_rate=100)
|
||||
|
||||
batchwise_item_details = {}
|
||||
for stock_doc in [ste1, ste2, ste3, ste4]:
|
||||
self.assertEqual(item_code, stock_doc.items[0].item_code)
|
||||
batchwise_item_details[stock_doc.items[0].batch_no] = [stock_doc.items[0].qty, 0.01]
|
||||
|
||||
stock_balance = frappe.get_all("Stock Ledger Entry",
|
||||
filters = {"item_code": item_code, "warehouse": warehouse},
|
||||
fields=["sum(stock_value_difference)"], as_list=1)
|
||||
|
||||
self.assertEqual(flt(stock_balance[0][0]), 6200.00)
|
||||
|
||||
sr = create_stock_reconciliation(item_code=item_code,
|
||||
warehouse = warehouse, batch_details = batchwise_item_details)
|
||||
|
||||
stock_balance = frappe.get_all("Stock Ledger Entry",
|
||||
filters = {"item_code": item_code, "warehouse": warehouse},
|
||||
fields=["sum(stock_value_difference)"], as_list=1)
|
||||
|
||||
self.assertEqual(flt(stock_balance[0][0]), 0.15)
|
||||
|
||||
for doc in [sr, ste1, ste2, ste3, ste4]:
|
||||
doc.cancel()
|
||||
frappe.delete_doc(doc.doctype, doc.name)
|
||||
|
||||
def insert_existing_sle(warehouse):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
@@ -251,20 +407,33 @@ def create_stock_reconciliation(**args):
|
||||
or frappe.get_cached_value("Company", sr.company, "cost_center") \
|
||||
or "_Test Cost Center - _TC"
|
||||
|
||||
sr.append("items", {
|
||||
"item_code": args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty,
|
||||
"valuation_rate": args.rate,
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no
|
||||
})
|
||||
if not args.batch_details:
|
||||
sr.append("items", {
|
||||
"item_code": args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty,
|
||||
"valuation_rate": args.rate,
|
||||
"serial_no": args.serial_no,
|
||||
"batch_no": args.batch_no
|
||||
})
|
||||
elif args.batch_details:
|
||||
for batch, data in args.batch_details.items():
|
||||
sr.append("items", {
|
||||
"item_code": args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": data[0],
|
||||
"valuation_rate": data[1],
|
||||
"batch_no": batch
|
||||
})
|
||||
|
||||
if not args.do_not_save:
|
||||
sr.insert()
|
||||
try:
|
||||
if not args.do_not_submit:
|
||||
sr.submit()
|
||||
except EmptyStockReconciliationItemsError:
|
||||
pass
|
||||
|
||||
try:
|
||||
if not args.do_not_submit:
|
||||
sr.submit()
|
||||
except EmptyStockReconciliationItemsError:
|
||||
pass
|
||||
return sr
|
||||
|
||||
def set_valuation_method(item_code, valuation_method):
|
||||
|
||||
@@ -20,6 +20,13 @@ frappe.ready(() => {
|
||||
options: 'Email',
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldtype: 'Data',
|
||||
label: __('Phone Number'),
|
||||
fieldname: 'phone',
|
||||
options: 'Phone',
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldtype: 'Data',
|
||||
label: __('Subject'),
|
||||
|
||||
Reference in New Issue
Block a user