fix: merge conflict
This commit is contained in:
commit
a57976660a
@ -33,6 +33,8 @@ def get_shipping_address(company, address = None):
|
||||
if address and frappe.db.get_value('Dynamic Link',
|
||||
{'parent': address, 'link_name': company}):
|
||||
filters.append(["Address", "name", "=", address])
|
||||
if not address:
|
||||
filters.append(["Address", "is_shipping_address", "=", 1])
|
||||
|
||||
address = frappe.get_all("Address", filters=filters, fields=fields) or {}
|
||||
|
||||
|
@ -257,9 +257,10 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If enabled, ledger entries will be posted for change amount in POS transactions",
|
||||
"fieldname": "post_change_gl_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Post Ledger Entries for Given Change"
|
||||
"label": "Create Ledger Entries for Change Amount"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -267,7 +268,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-25 12:34:05.858669",
|
||||
"modified": "2021-06-17 20:26:03.721202",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -54,7 +54,7 @@ class CForm(Document):
|
||||
frappe.throw(_("Please enter atleast 1 invoice in the table"))
|
||||
|
||||
def set_total_invoiced_amount(self):
|
||||
total = sum([flt(d.grand_total) for d in self.get('invoices')])
|
||||
total = sum(flt(d.grand_total) for d in self.get('invoices'))
|
||||
frappe.db.set(self, 'total_invoiced_amount', total)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -14,7 +14,7 @@ class CouponCode(Document):
|
||||
|
||||
if not self.coupon_code:
|
||||
if self.coupon_type == "Promotional":
|
||||
self.coupon_code =''.join([i for i in self.coupon_name if not i.isdigit()])[0:8].upper()
|
||||
self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
|
||||
elif self.coupon_type == "Gift Card":
|
||||
self.coupon_code = frappe.generate_hash()[:10].upper()
|
||||
|
||||
|
@ -86,7 +86,7 @@ def resolve_dunning(doc, state):
|
||||
for reference in doc.references:
|
||||
if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
|
||||
dunnings = frappe.get_list('Dunning', filters={
|
||||
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')})
|
||||
'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
|
||||
|
||||
for dunning in dunnings:
|
||||
frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
|
||||
|
@ -121,8 +121,7 @@ class GLEntry(Document):
|
||||
|
||||
def check_pl_account(self):
|
||||
if self.is_opening=='Yes' and \
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss" and \
|
||||
self.voucher_type not in ['Purchase Invoice', 'Sales Invoice']:
|
||||
frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
|
||||
frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
|
||||
|
@ -42,7 +42,7 @@ class InvoiceDiscounting(AccountsController):
|
||||
record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
|
||||
|
||||
def calculate_total_amount(self):
|
||||
self.total_amount = sum([flt(d.outstanding_amount) for d in self.invoices])
|
||||
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
|
||||
|
||||
def on_submit(self):
|
||||
self.update_sales_invoice()
|
||||
|
@ -196,8 +196,8 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account))
|
||||
|
||||
def check_credit_limit(self):
|
||||
customers = list(set([d.party for d in self.get("accounts")
|
||||
if d.party_type=="Customer" and d.party and flt(d.debit) > 0]))
|
||||
customers = list(set(d.party for d in self.get("accounts")
|
||||
if d.party_type=="Customer" and d.party and flt(d.debit) > 0))
|
||||
if customers:
|
||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||
for customer in customers:
|
||||
|
@ -21,7 +21,7 @@ class MonthlyDistribution(Document):
|
||||
idx += 1
|
||||
|
||||
def validate(self):
|
||||
total = sum([flt(d.percentage_allocation) for d in self.get("percentages")])
|
||||
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
|
||||
|
||||
if flt(total, 2) != 100.0:
|
||||
frappe.throw(_("Percentage Allocation should be equal to 100%") + \
|
||||
|
@ -310,7 +310,7 @@ class PaymentEntry(AccountsController):
|
||||
for k, v in no_oustanding_refs.items():
|
||||
frappe.msgprint(
|
||||
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
|
||||
.format(k, frappe.bold(", ".join([d.reference_name for d in v])), frappe.bold("negative outstanding amount"))
|
||||
.format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount"))
|
||||
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
|
||||
title=_("Warning"), indicator="orange")
|
||||
|
||||
@ -511,7 +511,7 @@ class PaymentEntry(AccountsController):
|
||||
def set_unallocated_amount(self):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
if self.payment_type == "Receive" \
|
||||
and self.base_total_allocated_amount < self.base_received_amount_after_tax + total_deductions \
|
||||
and self.total_allocated_amount < self.paid_amount_after_tax + (total_deductions / self.source_exchange_rate):
|
||||
@ -536,7 +536,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
self.difference_amount = self.base_paid_amount_after_tax - flt(self.base_received_amount_after_tax)
|
||||
|
||||
total_deductions = sum([flt(d.amount) for d in self.get("deductions")])
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
|
||||
self.difference_amount = flt(self.difference_amount - total_deductions,
|
||||
self.precision("difference_amount"))
|
||||
@ -552,8 +552,8 @@ class PaymentEntry(AccountsController):
|
||||
if ((self.payment_type=="Pay" and self.party_type=="Customer")
|
||||
or (self.payment_type=="Receive" and self.party_type=="Supplier")):
|
||||
|
||||
total_negative_outstanding = sum([abs(flt(d.outstanding_amount))
|
||||
for d in self.get("references") if flt(d.outstanding_amount) < 0])
|
||||
total_negative_outstanding = sum(abs(flt(d.outstanding_amount))
|
||||
for d in self.get("references") if flt(d.outstanding_amount) < 0)
|
||||
|
||||
paid_amount = self.paid_amount if self.payment_type=="Receive" else self.received_amount
|
||||
additional_charges = sum([flt(d.amount) for d in self.deductions])
|
||||
|
@ -112,7 +112,7 @@ class PaymentRequest(Document):
|
||||
if not data_of_completed_requests:
|
||||
return self.grand_total
|
||||
|
||||
request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
|
||||
request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests)
|
||||
return request_amounts
|
||||
|
||||
def on_cancel(self):
|
||||
|
@ -26,7 +26,7 @@ class PaymentTermsTemplate(Document):
|
||||
def check_duplicate_terms(self):
|
||||
terms = []
|
||||
for term in self.terms:
|
||||
term_info = (term.credit_days, term.credit_months, term.due_date_based_on)
|
||||
term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on)
|
||||
if term_info in terms:
|
||||
frappe.msgprint(
|
||||
_('The Payment Term at row {0} is possibly a duplicate.').format(term.idx),
|
||||
|
@ -183,7 +183,7 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
||||
condition = "ifnull({table}.{field}, '') in ({parent_groups})".format(
|
||||
table=table,
|
||||
field=field,
|
||||
parent_groups=", ".join([frappe.db.escape(d) for d in parent_groups])
|
||||
parent_groups=", ".join(frappe.db.escape(d) for d in parent_groups)
|
||||
)
|
||||
|
||||
frappe.flags.tree_conditions[key] = condition
|
||||
@ -264,7 +264,7 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
|
||||
# find pricing rule with highest priority
|
||||
if pricing_rules:
|
||||
max_priority = max([cint(p.priority) for p in pricing_rules])
|
||||
max_priority = max(cint(p.priority) for p in pricing_rules)
|
||||
if max_priority:
|
||||
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
|
||||
|
||||
@ -272,14 +272,14 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
pricing_rules = list(pricing_rules)
|
||||
|
||||
if len(pricing_rules) > 1:
|
||||
rate_or_discount = list(set([d.rate_or_discount for d in pricing_rules]))
|
||||
rate_or_discount = list(set(d.rate_or_discount for d in pricing_rules))
|
||||
if len(rate_or_discount) == 1 and rate_or_discount[0] == "Discount Percentage":
|
||||
pricing_rules = list(filter(lambda x: x.for_price_list==args.price_list, pricing_rules)) \
|
||||
or pricing_rules
|
||||
|
||||
if len(pricing_rules) > 1 and not args.for_shopping_cart:
|
||||
frappe.throw(_("Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}")
|
||||
.format("\n".join([d.name for d in pricing_rules])), MultiplePricingRuleConflict)
|
||||
.format("\n".join(d.name for d in pricing_rules)), MultiplePricingRuleConflict)
|
||||
elif pricing_rules:
|
||||
return pricing_rules[0]
|
||||
|
||||
@ -541,7 +541,7 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
|
||||
|
||||
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
|
||||
if pricing_rule_args:
|
||||
items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item])
|
||||
items = tuple((d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item)
|
||||
|
||||
for args in pricing_rule_args:
|
||||
if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
|
||||
|
@ -27,10 +27,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
});
|
||||
},
|
||||
|
||||
company: function() {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
},
|
||||
|
||||
onload: function() {
|
||||
this._super();
|
||||
|
||||
@ -569,5 +565,9 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
frm: frm,
|
||||
freeze_message: __("Creating Purchase Receipt ...")
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
},
|
||||
})
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -400,6 +400,7 @@ class PurchaseInvoice(BuyingController):
|
||||
# because updating ordered qty in bin depends upon updated ordered qty in PO
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.set_consumed_qty_in_po()
|
||||
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
|
||||
update_serial_nos_after_submit(self, "items")
|
||||
|
||||
@ -998,6 +999,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.update_stock_ledger()
|
||||
self.delete_auto_created_batches()
|
||||
self.set_consumed_qty_in_po()
|
||||
|
||||
self.make_gl_entries_on_cancel()
|
||||
|
||||
|
@ -621,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
self.assertEqual(actual_qty_0, get_qty_after_transaction())
|
||||
|
||||
def test_subcontracting_via_purchase_invoice(self):
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
update_backflush_based_on('BOM')
|
||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
|
||||
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
|
||||
qty=100, basic_rate=100)
|
||||
@ -632,7 +634,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(pi.get("supplied_items")), 2)
|
||||
|
||||
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
|
||||
rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
|
||||
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
|
||||
|
||||
def test_rejected_serial_no(self):
|
||||
|
@ -272,7 +272,7 @@
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate ",
|
||||
"label": "Rate",
|
||||
"oldfieldname": "import_rate",
|
||||
"oldfieldtype": "Currency",
|
||||
"options": "currency",
|
||||
@ -854,7 +854,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-30 09:02:39.256602",
|
||||
"modified": "2021-06-16 19:57:03.101571",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -112,7 +112,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
|
||||
si.submit()
|
||||
|
||||
tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
|
||||
tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC')
|
||||
self.assertEqual(tcs_charged, 500)
|
||||
invoices.append(si)
|
||||
|
||||
|
@ -101,7 +101,7 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
|
||||
def check_if_in_list(gle, gl_map, dimensions=None):
|
||||
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
|
||||
'cost_center', 'project']
|
||||
'cost_center', 'project', 'voucher_detail_no']
|
||||
|
||||
if dimensions:
|
||||
account_head_fieldnames = account_head_fieldnames + dimensions
|
||||
@ -143,7 +143,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||
validate_expense_against_budget(args)
|
||||
|
||||
def validate_cwip_accounts(gl_map):
|
||||
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
|
||||
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
|
||||
|
||||
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
|
||||
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
|
||||
|
@ -32,7 +32,7 @@ def get_accounts_in_mappers(mapping_names):
|
||||
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
|
||||
where cfma.parent in (%s)
|
||||
order by cfm.is_working_capital
|
||||
''', (', '.join(['"%s"' % d for d in mapping_names])))
|
||||
''', (', '.join('"%s"' % d for d in mapping_names)))
|
||||
|
||||
|
||||
def setup_mappers(mappers):
|
||||
@ -83,8 +83,8 @@ def setup_mappers(mappers):
|
||||
|
||||
account_types_labels = sorted(
|
||||
set(
|
||||
[(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
|
||||
for d in account_types]
|
||||
(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
|
||||
for d in account_types
|
||||
),
|
||||
key=lambda x: x[1]
|
||||
)
|
||||
@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
|
||||
total = 0
|
||||
for period in period_list:
|
||||
start_date = get_start_date(period, accumulated_values, company)
|
||||
accounts = ', '.join(['"%s"' % d for d in account_names])
|
||||
accounts = ', '.join('"%s"' % d for d in account_names)
|
||||
|
||||
if opening_balances:
|
||||
date_info = dict(date=start_date)
|
||||
|
@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object):
|
||||
out = []
|
||||
for party, row in iteritems(self.party_data):
|
||||
if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount:
|
||||
total_party_adjustment = sum([amount for amount in itervalues(self.party_adjustment_details.get(party, {}))])
|
||||
total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {})))
|
||||
row.paid_amount -= total_party_adjustment
|
||||
|
||||
adjustments = self.party_adjustment_details.get(party, {})
|
||||
|
@ -369,7 +369,7 @@ def set_gl_entries_by_account(
|
||||
|
||||
if accounts:
|
||||
additional_conditions += " and account in ({})"\
|
||||
.format(", ".join([frappe.db.escape(d) for d in accounts]))
|
||||
.format(", ".join(frappe.db.escape(d) for d in accounts))
|
||||
|
||||
gl_filters = {
|
||||
"company": company,
|
||||
|
@ -36,16 +36,12 @@ frappe.query_reports["General Ledger"] = {
|
||||
{
|
||||
"fieldname":"account",
|
||||
"label": __("Account"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "MultiSelectList",
|
||||
"options": "Account",
|
||||
"get_query": function() {
|
||||
var company = frappe.query_report.get_filter_value('company');
|
||||
return {
|
||||
"doctype": "Account",
|
||||
"filters": {
|
||||
"company": company,
|
||||
}
|
||||
}
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Account', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -135,7 +131,9 @@ frappe.query_reports["General Ledger"] = {
|
||||
"label": __("Cost Center"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Cost Center', txt);
|
||||
return frappe.db.get_link_options('Cost Center', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -143,7 +141,9 @@ frappe.query_reports["General Ledger"] = {
|
||||
"label": __("Project"),
|
||||
"fieldtype": "MultiSelectList",
|
||||
get_data: function(txt) {
|
||||
return frappe.db.get_link_options('Project', txt);
|
||||
return frappe.db.get_link_options('Project', txt, {
|
||||
company: frappe.query_report.get_filter_value("company")
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -49,8 +49,12 @@ def validate_filters(filters, account_details):
|
||||
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))
|
||||
for account in filters.account:
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if filters.get('account'):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
|
||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')
|
||||
and account_details[filters.account].is_group == 0):
|
||||
@ -87,7 +91,19 @@ def set_account_currency(filters):
|
||||
account_currency = None
|
||||
|
||||
if filters.get("account"):
|
||||
account_currency = get_account_currency(filters.account)
|
||||
if len(filters.get("account")) == 1:
|
||||
account_currency = get_account_currency(filters.account[0])
|
||||
else:
|
||||
currency = get_account_currency(filters.account[0])
|
||||
is_same_account_currency = True
|
||||
for account in filters.get("account"):
|
||||
if get_account_currency(account) != currency:
|
||||
is_same_account_currency = False
|
||||
break
|
||||
|
||||
if is_same_account_currency:
|
||||
account_currency = currency
|
||||
|
||||
elif filters.get("party"):
|
||||
gle_currency = frappe.db.get_value(
|
||||
"GL Entry", {
|
||||
@ -205,10 +221,10 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
if filters.get("account") and not filters.get("include_dimensions"):
|
||||
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
|
||||
conditions.append("""account in (select name from tabAccount
|
||||
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
|
||||
filters.account = get_accounts_with_children(filters.account)
|
||||
conditions.append("account in %(account)s")
|
||||
|
||||
if filters.get("cost_center"):
|
||||
filters.cost_center = get_cost_centers_with_children(filters.cost_center)
|
||||
@ -266,6 +282,20 @@ def get_conditions(filters):
|
||||
|
||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
def get_accounts_with_children(accounts):
|
||||
if not isinstance(accounts, list):
|
||||
accounts = [d.strip() for d in accounts.strip().split(',') if d]
|
||||
|
||||
all_accounts = []
|
||||
for d in accounts:
|
||||
if frappe.db.exists("Account", d):
|
||||
lft, rgt = frappe.db.get_value("Account", d, ["lft", "rgt"])
|
||||
children = frappe.get_all("Account", filters={"lft": [">=", lft], "rgt": ["<=", rgt]})
|
||||
all_accounts += [c.name for c in children]
|
||||
else:
|
||||
frappe.throw(_("Account: {0} does not exist").format(d))
|
||||
|
||||
return list(set(all_accounts))
|
||||
|
||||
def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries):
|
||||
data = []
|
||||
|
@ -334,7 +334,7 @@ def get_aii_accounts():
|
||||
|
||||
def get_purchase_receipts_against_purchase_order(item_list):
|
||||
po_pr_map = frappe._dict()
|
||||
po_item_rows = list(set([d.po_detail for d in item_list]))
|
||||
po_item_rows = list(set(d.po_detail for d in item_list))
|
||||
|
||||
if po_item_rows:
|
||||
purchase_receipts = frappe.db.sql("""
|
||||
|
@ -23,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
if item_list:
|
||||
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
|
||||
|
||||
mode_of_payments = get_mode_of_payments(set([d.parent for d in item_list]))
|
||||
mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
|
||||
so_dn_map = get_delivery_notes_against_sales_order(item_list)
|
||||
|
||||
data = []
|
||||
|
@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field):
|
||||
), filters, as_dict=1)
|
||||
|
||||
def concat_mode_of_payments(pos_entries):
|
||||
mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries]))
|
||||
mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries))
|
||||
for entry in pos_entries:
|
||||
if mode_of_payments.get(entry.pos_invoice):
|
||||
entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, []))
|
||||
|
||||
def add_subtotal_row(data, group_invoices, group_by_field, group_by_value):
|
||||
grand_total = sum([d.grand_total for d in group_invoices])
|
||||
paid_amount = sum([d.paid_amount for d in group_invoices])
|
||||
grand_total = sum(d.grand_total for d in group_invoices)
|
||||
paid_amount = sum(d.paid_amount for d in group_invoices)
|
||||
data.append({
|
||||
group_by_field: group_by_value,
|
||||
"grand_total": grand_total,
|
||||
|
@ -26,7 +26,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
|
||||
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
|
||||
invoice_expense_map, expense_accounts)
|
||||
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
|
||||
suppliers = list(set([d.supplier for d in invoice_list]))
|
||||
suppliers = list(set(d.supplier for d in invoice_list))
|
||||
supplier_details = get_supplier_details(suppliers)
|
||||
|
||||
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
|
||||
@ -120,13 +120,13 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
and docstatus = 1 and (account_head is not null and account_head != '')
|
||||
and category in ('Total', 'Valuation and Total')
|
||||
and parent in (%s) order by account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
|
||||
unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts]
|
||||
@ -208,7 +208,7 @@ def get_invoice_expense_map(invoice_list):
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
group by parent, expense_account
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_expense_map = {}
|
||||
for d in expense_details:
|
||||
@ -221,7 +221,7 @@ def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
|
||||
and is_internal_supplier = 1 and company = represents_company""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
internal_invoice_map = {}
|
||||
for d in unrealized_amount_details:
|
||||
@ -238,7 +238,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
|
||||
where parent in (%s) and category in ('Total', 'Valuation and Total')
|
||||
and base_tax_amount_after_discount_amount != 0
|
||||
group by parent, account_head, add_deduct_tax
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_tax_map = {}
|
||||
for d in tax_details:
|
||||
@ -258,7 +258,7 @@ def get_invoice_po_pr_map(invoice_list):
|
||||
select parent, purchase_order, purchase_receipt, po_detail, project
|
||||
from `tabPurchase Invoice Item`
|
||||
where parent in (%s)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
""" % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_po_pr_map = {}
|
||||
for d in pi_items:
|
||||
|
@ -158,7 +158,7 @@ def get_sales_invoice_data(filters):
|
||||
def get_mode_of_payments(filters):
|
||||
mode_of_payments = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
|
||||
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
|
||||
from `tabSales Invoice` a, `tabSales Invoice Payment` b
|
||||
@ -197,7 +197,7 @@ def get_invoices(filters):
|
||||
def get_mode_of_payment_details(filters):
|
||||
mode_of_payment_details = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
|
||||
invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
|
||||
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
|
||||
|
@ -248,19 +248,19 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
income_accounts = frappe.db.sql_list("""select distinct income_account
|
||||
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
|
||||
order by income_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
tax_accounts = frappe.db.sql_list("""select distinct account_head
|
||||
from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
|
||||
and docstatus = 1 and base_tax_amount_after_discount_amount != 0
|
||||
and parent in (%s) order by account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
|
||||
from `tabSales Invoice` where docstatus = 1 and name in (%s)
|
||||
and ifnull(unrealized_profit_loss_account, '') != ''
|
||||
order by unrealized_profit_loss_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
|
||||
|
||||
for account in income_accounts:
|
||||
income_columns.append({
|
||||
@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns):
|
||||
def get_invoice_income_map(invoice_list):
|
||||
income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount
|
||||
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_income_map = {}
|
||||
for d in income_details:
|
||||
@ -419,7 +419,7 @@ def get_internal_invoice_map(invoice_list):
|
||||
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
|
||||
base_net_total as amount from `tabSales Invoice` where name in (%s)
|
||||
and is_internal_customer = 1 and company = represents_company""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
internal_invoice_map = {}
|
||||
for d in unrealized_amount_details:
|
||||
@ -432,7 +432,7 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
|
||||
tax_details = frappe.db.sql("""select parent, account_head,
|
||||
sum(base_tax_amount_after_discount_amount) as tax_amount
|
||||
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_tax_map = {}
|
||||
for d in tax_details:
|
||||
@ -451,7 +451,7 @@ def get_invoice_so_dn_map(invoice_list):
|
||||
si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_so_dn_map = {}
|
||||
for d in si_items:
|
||||
@ -475,7 +475,7 @@ def get_invoice_cc_wh_map(invoice_list):
|
||||
si_items = frappe.db.sql("""select parent, cost_center, warehouse
|
||||
from `tabSales Invoice Item` where parent in (%s)
|
||||
and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" %
|
||||
', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
|
||||
', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
|
||||
|
||||
invoice_cc_wh_map = {}
|
||||
for d in si_items:
|
||||
|
@ -78,7 +78,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
and company=%s and posting_date between %s and %s
|
||||
""", (supplier, company, from_date, to_date), as_dict=1)
|
||||
|
||||
supplier_credit_amount = flt(sum([d.credit for d in entries]))
|
||||
supplier_credit_amount = flt(sum(d.credit for d in entries))
|
||||
|
||||
vouchers = [d.voucher_no for d in entries]
|
||||
vouchers += get_advance_vouchers([supplier], company=company,
|
||||
@ -91,7 +91,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
|
||||
from `tabGL Entry`
|
||||
where account=%s and posting_date between %s and %s
|
||||
and company=%s and credit > 0 and voucher_no in ({0})
|
||||
""".format(', '.join(["'%s'" % d for d in vouchers])),
|
||||
""".format(', '.join("'%s'" % d for d in vouchers)),
|
||||
(account, from_date, to_date, company))[0][0])
|
||||
|
||||
date_range_filter = [fiscal_year, from_date, to_date]
|
||||
|
@ -139,6 +139,6 @@ def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=N
|
||||
gross_profit_data = GrossProfitGenerator(filters)
|
||||
result = gross_profit_data.grouped_data
|
||||
if not with_item_data:
|
||||
result = sum([d.gross_profit for d in result])
|
||||
result = sum(d.gross_profit for d in result)
|
||||
|
||||
return result
|
||||
|
@ -635,7 +635,7 @@ def get_held_invoices(party_type, party):
|
||||
'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
|
||||
as_dict=1
|
||||
)
|
||||
held_invoices = set([d['name'] for d in held_invoices])
|
||||
held_invoices = set(d['name'] for d in held_invoices)
|
||||
|
||||
return held_invoices
|
||||
|
||||
|
@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase):
|
||||
})
|
||||
asset.insert()
|
||||
accumulated_depreciation_after_full_schedule = \
|
||||
max([d.accumulated_depreciation_amount for d in asset.get("schedules")])
|
||||
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
|
||||
|
||||
asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) -
|
||||
flt(accumulated_depreciation_after_full_schedule))
|
||||
|
@ -92,7 +92,7 @@ class AssetValueAdjustment(Document):
|
||||
d.value_after_depreciation = asset_value
|
||||
|
||||
if d.depreciation_method in ("Straight Line", "Manual"):
|
||||
end_date = max([s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx])
|
||||
end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
|
||||
total_days = date_diff(end_date, self.date)
|
||||
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
|
||||
from_date = self.date
|
||||
|
@ -9,13 +9,14 @@
|
||||
"supp_master_name",
|
||||
"supplier_group",
|
||||
"buying_price_list",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"column_break_3",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"maintain_same_rate",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"allow_multiple_items",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"subcontract",
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
@ -108,6 +109,13 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Override Stop Action",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "If checked, Rejected Quantity will be included while making Purchase Invoice from Purchase Receipt.",
|
||||
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"fieldtype": "Check",
|
||||
"label": "Bill for Rejected Quantity in Purchase Invoice"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -115,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-04 20:01:44.087066",
|
||||
"modified": "2021-06-24 10:38:28.934525",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -53,6 +53,39 @@ frappe.ui.form.on("Purchase Order", {
|
||||
} else {
|
||||
frm.set_value("tax_withholding_category", frm.supplier_tds);
|
||||
}
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.trigger('get_materials_from_supplier');
|
||||
},
|
||||
|
||||
get_materials_from_supplier: function(frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
|
||||
frm.doc.supplied_items.forEach(d => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (po_details && po_details.length) {
|
||||
frm.add_custom_button(__('Return of Components'), () => {
|
||||
frm.call({
|
||||
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
|
||||
freeze: true,
|
||||
freeze_message: __('Creating Stock Entry'),
|
||||
args: { purchase_order: frm.doc.name, po_details: po_details },
|
||||
callback: function(r) {
|
||||
if (r && r.message) {
|
||||
const doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
}
|
||||
});
|
||||
}, __('Create'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -217,7 +250,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
},
|
||||
|
||||
has_unsupplied_items: function() {
|
||||
return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty)
|
||||
return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty)
|
||||
},
|
||||
|
||||
make_stock_entry: function() {
|
||||
@ -513,12 +546,14 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend(
|
||||
],
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
|
||||
|
||||
frappe.call({
|
||||
method: "frappe.desk.form.utils.add_comment",
|
||||
args: {
|
||||
reference_doctype: me.frm.doctype,
|
||||
reference_name: me.frm.docname,
|
||||
content: __('Reason for hold:') + " " +data.reason_for_hold,
|
||||
content: __(reason_for_hold),
|
||||
comment_email: frappe.session.user,
|
||||
comment_by: frappe.session.user_fullname
|
||||
},
|
||||
|
@ -609,6 +609,7 @@
|
||||
"fieldname": "supplied_items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Supplied Items",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "po_raw_material_details",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Purchase Order Item Supplied",
|
||||
@ -1377,7 +1378,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-19 00:55:30.781375",
|
||||
"modified": "2021-05-30 15:17:53.663648",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
@ -14,12 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
|
||||
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
|
||||
from erpnext.stock.utils import get_bin
|
||||
from erpnext.accounts.party import get_party_account_currency
|
||||
from six import string_types
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
|
||||
unlink_inter_company_doc
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
|
||||
update_linked_doc, unlink_inter_company_doc)
|
||||
|
||||
form_grid_templates = {
|
||||
"items": "templates/form_grid/item_grid.html"
|
||||
@ -139,7 +138,7 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
def validate_minimum_order_qty(self):
|
||||
if not self.get("items"): return
|
||||
items = list(set([d.item_code for d in self.get("items")]))
|
||||
items = list(set(d.item_code for d in self.get("items")))
|
||||
|
||||
itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty
|
||||
from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items))
|
||||
@ -326,10 +325,10 @@ class PurchaseOrder(BuyingController):
|
||||
so.notify_update()
|
||||
|
||||
def has_drop_ship_item(self):
|
||||
return any([d.delivered_by_supplier for d in self.items])
|
||||
return any(d.delivered_by_supplier for d in self.items)
|
||||
|
||||
def is_against_so(self):
|
||||
return any([d.sales_order for d in self.items if d.sales_order])
|
||||
return any(d.sales_order for d in self.items if d.sales_order)
|
||||
|
||||
def set_received_qty_for_drop_ship_items(self):
|
||||
for item in self.items:
|
||||
@ -503,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_rm_stock_entry(purchase_order, rm_items):
|
||||
if isinstance(rm_items, string_types):
|
||||
rm_items_list = rm_items
|
||||
|
||||
if isinstance(rm_items, str):
|
||||
rm_items_list = json.loads(rm_items)
|
||||
else:
|
||||
elif not rm_items:
|
||||
frappe.throw(_("No Items available for transfer"))
|
||||
|
||||
if rm_items_list:
|
||||
@ -543,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
|
||||
'qty': rm_item_data["qty"],
|
||||
'from_warehouse': rm_item_data["warehouse"],
|
||||
'stock_uom': rm_item_data["stock_uom"],
|
||||
'serial_no': rm_item_data.get('serial_no'),
|
||||
'batch_no': rm_item_data.get('batch_no'),
|
||||
'main_item_code': rm_item_data["item_code"],
|
||||
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
|
||||
}
|
||||
@ -582,3 +585,58 @@ def update_status(status, name):
|
||||
def make_inter_company_sales_order(source_name, target_doc=None):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
|
||||
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_materials_from_supplier(purchase_order, po_details):
|
||||
if isinstance(po_details, str):
|
||||
po_details = json.loads(po_details)
|
||||
|
||||
doc = frappe.get_cached_doc('Purchase Order', purchase_order)
|
||||
doc.initialized_fields()
|
||||
doc.purchase_orders = [doc.name]
|
||||
doc.get_available_materials()
|
||||
|
||||
if not doc.available_materials:
|
||||
frappe.throw(_('Materials are already received against the purchase order {0}')
|
||||
.format(purchase_order))
|
||||
|
||||
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
|
||||
|
||||
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
|
||||
ste_doc = frappe.new_doc('Stock Entry')
|
||||
ste_doc.purpose = 'Material Transfer'
|
||||
ste_doc.purchase_order = po_doc.name
|
||||
ste_doc.company = po_doc.company
|
||||
ste_doc.is_return = 1
|
||||
|
||||
for key, value in available_materials.items():
|
||||
if not value.qty:
|
||||
continue
|
||||
|
||||
if value.batch_no:
|
||||
for batch_no, qty in value.batch_no.items():
|
||||
if qty > 0:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
|
||||
else:
|
||||
add_items_in_ste(ste_doc, value, value.qty, po_details)
|
||||
|
||||
ste_doc.set_stock_entry_type()
|
||||
ste_doc.calculate_rate_and_amount()
|
||||
|
||||
return ste_doc
|
||||
|
||||
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
|
||||
item = ste_doc.append('items', row.item_details)
|
||||
|
||||
po_detail = list(set(row.po_details).intersection(po_details))
|
||||
item.update({
|
||||
'qty': qty,
|
||||
'batch_no': batch_no,
|
||||
'basic_rate': row.item_details['rate'],
|
||||
'po_detail': po_detail[0] if po_detail else '',
|
||||
's_warehouse': row.item_details['t_warehouse'],
|
||||
't_warehouse': row.item_details['s_warehouse'],
|
||||
'item_code': row.item_details['rm_item_code'],
|
||||
'subcontracted_item': row.item_details['main_item_code'],
|
||||
'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
|
||||
})
|
@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
|
||||
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):
|
||||
@ -359,7 +358,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
update_child_qty_rate('Purchase Order', trans_item, po.name)
|
||||
po.reload()
|
||||
|
||||
total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
|
||||
total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items"))
|
||||
|
||||
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
|
||||
|
||||
@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
|
||||
|
||||
def test_exploded_items_in_subcontracted(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
item_code = "_Test Subcontracted FG Item 11"
|
||||
make_subcontracted_item(item_code=item_code)
|
||||
|
||||
po = create_purchase_order(item_code=item_code, qty=1,
|
||||
@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
|
||||
for item in rm_items:
|
||||
transferred_rm_map[item.get('rm_item_code')] = item
|
||||
|
||||
for item in pr.get('supplied_items'):
|
||||
self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
|
||||
|
||||
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_supplied_qty_against_subcontracted_po(self):
|
||||
@ -1117,6 +1043,10 @@ def create_purchase_order(**args):
|
||||
po.conversion_factor = args.conversion_factor or 1
|
||||
po.supplier_warehouse = args.supplier_warehouse or None
|
||||
|
||||
if args.rm_items:
|
||||
for row in args.rm_items:
|
||||
po.append("items", row)
|
||||
else:
|
||||
po.append("items", {
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
@ -1126,12 +1056,15 @@ def create_purchase_order(**args):
|
||||
"include_exploded_items": args.get('include_exploded_items', 1),
|
||||
"against_blanket_order": args.against_blanket_order
|
||||
})
|
||||
|
||||
po.set_missing_values()
|
||||
if not args.do_not_save:
|
||||
po.insert()
|
||||
if not args.do_not_submit:
|
||||
if po.is_subcontracted == "Yes":
|
||||
supp_items = po.get("supplied_items")
|
||||
for d in supp_items:
|
||||
if not d.reserve_warehouse:
|
||||
d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
|
||||
po.submit()
|
||||
|
||||
|
@ -6,21 +6,25 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"bom_detail_no",
|
||||
"rm_item_code",
|
||||
"column_break_3",
|
||||
"stock_uom",
|
||||
"reserve_warehouse",
|
||||
"conversion_factor",
|
||||
"column_break_6",
|
||||
"rm_item_code",
|
||||
"bom_detail_no",
|
||||
"reference_name",
|
||||
"reserve_warehouse",
|
||||
"section_break2",
|
||||
"rate",
|
||||
"col_break2",
|
||||
"amount",
|
||||
"section_break1",
|
||||
"required_qty",
|
||||
"supplied_qty",
|
||||
"col_break1",
|
||||
"supplied_qty"
|
||||
"consumed_qty",
|
||||
"returned_qty",
|
||||
"total_supplied_qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -125,6 +129,8 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Supplied Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -142,13 +148,42 @@
|
||||
{
|
||||
"fieldname": "col_break2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Returned Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_supplied_qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Total Supplied Qty",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-09 15:17:58.128242",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item Supplied",
|
||||
|
@ -6,10 +6,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"main_item_code",
|
||||
"description",
|
||||
"rm_item_code",
|
||||
"item_name",
|
||||
"bom_detail_no",
|
||||
"col_break1",
|
||||
"rm_item_code",
|
||||
"description",
|
||||
"stock_uom",
|
||||
"conversion_factor",
|
||||
"reference_name",
|
||||
@ -25,7 +26,8 @@
|
||||
"secbreak_3",
|
||||
"batch_no",
|
||||
"col_break4",
|
||||
"serial_no"
|
||||
"serial_no",
|
||||
"purchase_order"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -52,7 +54,6 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"oldfieldname": "description",
|
||||
"oldfieldtype": "Data",
|
||||
@ -81,18 +82,20 @@
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"label": "Available Qty For Consumption",
|
||||
"oldfieldname": "required_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"in_list_view": 1,
|
||||
"label": "Qty to Be Consumed",
|
||||
"oldfieldname": "consumed_qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -183,12 +186,28 @@
|
||||
{
|
||||
"fieldname": "col_break4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Item Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "purchase_order",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Purchase Order",
|
||||
"no_copy": 1,
|
||||
"options": "Purchase Order",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"modified": "2021-06-19 19:33:04.431213",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Receipt Item Supplied",
|
||||
|
@ -317,19 +317,21 @@ def add_items(sq_doc, supplier, items):
|
||||
create_rfq_items(sq_doc, supplier, data)
|
||||
|
||||
def create_rfq_items(sq_doc, supplier, data):
|
||||
sq_doc.append('items', {
|
||||
"item_code": data.item_code,
|
||||
"item_name": data.item_name,
|
||||
"description": data.description,
|
||||
"qty": data.qty,
|
||||
"rate": data.rate,
|
||||
"conversion_factor": data.conversion_factor if data.conversion_factor else None,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
|
||||
"warehouse": data.warehouse or '',
|
||||
args = {}
|
||||
|
||||
for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
|
||||
'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
|
||||
args[field] = data.get(field)
|
||||
|
||||
args.update({
|
||||
"request_for_quotation_item": data.name,
|
||||
"request_for_quotation": data.parent
|
||||
"request_for_quotation": data.parent,
|
||||
"supplier_part_no": frappe.db.get_value("Item Supplier",
|
||||
{'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
|
||||
})
|
||||
|
||||
sq_doc.append('items', args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pdf(doctype, name, supplier):
|
||||
doc = get_rfq_doc(doctype, name, supplier)
|
||||
@ -391,7 +393,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
|
||||
def get_supplier_tag():
|
||||
if not frappe.cache().hget("Supplier", "Tags"):
|
||||
filters = {"document_type": "Supplier"}
|
||||
tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag]))
|
||||
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
|
||||
frappe.cache().hset("Supplier", "Tags", tags)
|
||||
|
||||
return frappe.cache().hget("Supplier", "Tags")
|
||||
|
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Subcontract Order Summary"] = {
|
||||
"filters": [
|
||||
{
|
||||
label: __("Company"),
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("From Date"),
|
||||
fieldname:"from_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("To Date"),
|
||||
fieldname:"to_date",
|
||||
fieldtype: "Date",
|
||||
default: frappe.datetime.get_today(),
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
label: __("Purchase Order"),
|
||||
fieldname: "name",
|
||||
fieldtype: "Link",
|
||||
options: "Purchase Order",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: 1,
|
||||
is_subcontracted: 'Yes',
|
||||
company: frappe.query_report.get_filter_value('company')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
@ -0,0 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-05-31 14:43:32.417694",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-05-31 14:43:32.417694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Subcontract Order Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Purchase Order",
|
||||
"report_name": "Subcontract Order Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"role": "Purchase Manager"
|
||||
},
|
||||
{
|
||||
"role": "Purchase User"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(report_filters):
|
||||
data = []
|
||||
orders = get_subcontracted_orders(report_filters)
|
||||
|
||||
if orders:
|
||||
supplied_items = get_supplied_items(orders, report_filters)
|
||||
po_details = prepare_subcontracted_data(orders, supplied_items)
|
||||
get_subcontracted_data(po_details, data)
|
||||
|
||||
return data
|
||||
|
||||
def get_subcontracted_orders(report_filters):
|
||||
fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
|
||||
'`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
|
||||
'`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
|
||||
|
||||
filters = get_filters(report_filters)
|
||||
|
||||
return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
|
||||
|
||||
def get_filters(report_filters):
|
||||
filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
|
||||
['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
|
||||
|
||||
for field in ['name', 'company']:
|
||||
if report_filters.get(field):
|
||||
filters.append(['Purchase Order', field, '=', report_filters.get(field)])
|
||||
|
||||
return filters
|
||||
|
||||
def get_supplied_items(orders, report_filters):
|
||||
if not orders:
|
||||
return []
|
||||
|
||||
fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
|
||||
'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
|
||||
|
||||
filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
|
||||
|
||||
supplied_items = {}
|
||||
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
|
||||
new_key = (row.parent, row.reference_name, row.main_item_code)
|
||||
|
||||
supplied_items.setdefault(new_key, []).append(row)
|
||||
|
||||
return supplied_items
|
||||
|
||||
def prepare_subcontracted_data(orders, supplied_items):
|
||||
po_details = {}
|
||||
for row in orders:
|
||||
key = (row.po_id, row.name, row.item_code)
|
||||
if key not in po_details:
|
||||
po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
|
||||
|
||||
details = po_details[key]
|
||||
|
||||
if supplied_items.get(key):
|
||||
for supplied_item in supplied_items[key]:
|
||||
details['supplied_items'].append(supplied_item)
|
||||
|
||||
return po_details
|
||||
|
||||
def get_subcontracted_data(po_details, data):
|
||||
for key, details in po_details.items():
|
||||
res = details.po_item
|
||||
for index, row in enumerate(details.supplied_items):
|
||||
if index != 0:
|
||||
res = {}
|
||||
|
||||
res.update(row)
|
||||
data.append(res)
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"label": _("Purchase Order"),
|
||||
"fieldname": "po_id",
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Order",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"label": _("Status"),
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"width": 80
|
||||
},
|
||||
{
|
||||
"label": _("Subcontracted Item"),
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": _("Order Qty"),
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 90
|
||||
},
|
||||
{
|
||||
"label": _("Received Qty"),
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Supplied Item"),
|
||||
"fieldname": "rm_item_code",
|
||||
"fieldtype": "Link",
|
||||
"options": "Item",
|
||||
"width": 160
|
||||
},
|
||||
{
|
||||
"label": _("Required Qty"),
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Supplied Qty"),
|
||||
"fieldname": "supplied_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
},
|
||||
{
|
||||
"label": _("Consumed Qty"),
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"label": _("Returned Qty"),
|
||||
"fieldname": "returned_qty",
|
||||
"fieldtype": "Float",
|
||||
"width": 110
|
||||
}
|
||||
]
|
@ -9,10 +9,10 @@ def execute(filters=None):
|
||||
if filters.from_date >= filters.to_date:
|
||||
frappe.msgprint(_("To Date must be greater than From Date"))
|
||||
|
||||
data = []
|
||||
columns = get_columns()
|
||||
get_data(data , filters)
|
||||
return columns, data
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data or []
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
@ -21,13 +21,12 @@ def get_columns():
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "purchase_order",
|
||||
"options": "Purchase Order",
|
||||
"width": 150
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"label": _("Date"),
|
||||
"fieldtype": "Date",
|
||||
"fieldname": "date",
|
||||
"hidden": 1,
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
@ -41,97 +40,58 @@ def get_columns():
|
||||
"label": _("Item Code"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "rm_item_code",
|
||||
"width": 100
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": _("Required Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "r_qty",
|
||||
"width": 100
|
||||
"fieldname": "reqd_qty",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"label": _("Transferred Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "t_qty",
|
||||
"width": 100
|
||||
"fieldname": "transferred_qty",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"label": _("Pending Quantity"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "p_qty",
|
||||
"width": 100
|
||||
"width": 150
|
||||
}
|
||||
]
|
||||
|
||||
def get_data(data, filters):
|
||||
po = get_po(filters)
|
||||
po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po]))
|
||||
|
||||
sub_items = get_purchase_order_item_supplied([v.name for v in po])
|
||||
|
||||
for order in po:
|
||||
for item in sub_items:
|
||||
if order.name == item.parent and order.name in po_transferred_qty_map and \
|
||||
item.required_qty != po_transferred_qty_map.get(order.name).get(item.rm_item_code):
|
||||
transferred_qty = po_transferred_qty_map.get(order.name).get(item.rm_item_code) \
|
||||
if po_transferred_qty_map.get(order.name).get(item.rm_item_code) else 0
|
||||
row ={
|
||||
'purchase_order': item.parent,
|
||||
'date': order.transaction_date,
|
||||
'supplier': order.supplier,
|
||||
'rm_item_code': item.rm_item_code,
|
||||
'r_qty': item.required_qty,
|
||||
't_qty':transferred_qty,
|
||||
'p_qty':item.required_qty - transferred_qty
|
||||
}
|
||||
def get_data(filters):
|
||||
po_rm_item_details = get_po_items_to_supply(filters)
|
||||
|
||||
data = []
|
||||
for row in po_rm_item_details:
|
||||
transferred_qty = row.get("transferred_qty") or 0
|
||||
if transferred_qty < row.get("reqd_qty", 0):
|
||||
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
|
||||
row.p_qty = pending_qty if pending_qty > 0 else 0
|
||||
data.append(row)
|
||||
|
||||
return(data)
|
||||
return data
|
||||
|
||||
def get_po(filters):
|
||||
record_filters = [
|
||||
["is_subcontracted", "=", "Yes"],
|
||||
["supplier", "=", filters.supplier],
|
||||
["transaction_date", "<=", filters.to_date],
|
||||
["transaction_date", ">=", filters.from_date],
|
||||
["docstatus", "=", 1]
|
||||
]
|
||||
return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"])
|
||||
|
||||
def get_transferred_quantity(po_name):
|
||||
stock_entries = get_stock_entry(po_name)
|
||||
stock_entries_detail = get_stock_entry_detail([v.name for v in stock_entries])
|
||||
po_transferred_qty_map = {}
|
||||
|
||||
|
||||
for entry in stock_entries:
|
||||
for details in stock_entries_detail:
|
||||
if details.parent == entry.name:
|
||||
details["Purchase_order"] = entry.purchase_order
|
||||
if entry.purchase_order not in po_transferred_qty_map:
|
||||
po_transferred_qty_map[entry.purchase_order] = {}
|
||||
po_transferred_qty_map[entry.purchase_order][details.item_code] = details.qty
|
||||
else:
|
||||
po_transferred_qty_map[entry.purchase_order][details.item_code] = po_transferred_qty_map[entry.purchase_order].get(details.item_code, 0) + details.qty
|
||||
|
||||
return po_transferred_qty_map
|
||||
|
||||
|
||||
def get_stock_entry(po):
|
||||
return frappe.get_all("Stock Entry", filters=[
|
||||
('purchase_order', 'IN', po),
|
||||
('stock_entry_type', '=', 'Send to Subcontractor'),
|
||||
('docstatus', '=', 1)
|
||||
], fields=["name", "purchase_order"])
|
||||
|
||||
def get_stock_entry_detail(se):
|
||||
return frappe.get_all("Stock Entry Detail", filters=[
|
||||
["parent", "in", se]
|
||||
def get_po_items_to_supply(filters):
|
||||
return frappe.db.get_all(
|
||||
"Purchase Order",
|
||||
fields=[
|
||||
"name as purchase_order",
|
||||
"transaction_date as date",
|
||||
"supplier as supplier",
|
||||
"`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
|
||||
"`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
|
||||
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty"
|
||||
],
|
||||
fields=["parent", "item_code", "qty"])
|
||||
|
||||
def get_purchase_order_item_supplied(po):
|
||||
return frappe.get_all("Purchase Order Item Supplied", filters=[
|
||||
('parent', 'IN', po)
|
||||
], fields=['parent', 'rm_item_code', 'required_qty'])
|
||||
filters = [
|
||||
["Purchase Order", "per_received", "<", "100"],
|
||||
["Purchase Order", "is_subcontracted", "=", "Yes"],
|
||||
["Purchase Order", "supplier", "=", filters.supplier],
|
||||
["Purchase Order", "transaction_date", "<=", filters.to_date],
|
||||
["Purchase Order", "transaction_date", ">=", filters.from_date],
|
||||
["Purchase Order", "docstatus", "=", 1]
|
||||
]
|
||||
)
|
@ -12,34 +12,80 @@ import json, frappe, unittest
|
||||
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
|
||||
|
||||
def test_pending_and_transferred_qty(self):
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
|
||||
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
|
||||
|
||||
# Material Receipt of RMs
|
||||
make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
|
||||
transfer_subcontracted_raw_materials(po.name)
|
||||
col, data = execute(filters=frappe._dict({'supplier': po.supplier,
|
||||
|
||||
se = transfer_subcontracted_raw_materials(po)
|
||||
|
||||
col, data = execute(filters=frappe._dict(
|
||||
{
|
||||
'supplier': po.supplier,
|
||||
'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
|
||||
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))}))
|
||||
self.assertEqual(data[0]['purchase_order'], po.name)
|
||||
self.assertIn(data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
|
||||
self.assertIn(data[0]['p_qty'], [9, 18])
|
||||
self.assertIn(data[0]['t_qty'], [1, 2])
|
||||
'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))
|
||||
}
|
||||
))
|
||||
po.reload()
|
||||
|
||||
self.assertEqual(data[1]['purchase_order'], po.name)
|
||||
self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
|
||||
self.assertIn(data[1]['p_qty'], [9, 18])
|
||||
self.assertIn(data[1]['t_qty'], [1, 2])
|
||||
po_data = [row for row in data if row.get('purchase_order') == po.name]
|
||||
# Alphabetically sort to be certain of order
|
||||
po_data = sorted(po_data, key = lambda i: i['rm_item_code'])
|
||||
|
||||
self.assertEqual(len(po_data), 2)
|
||||
self.assertEqual(po_data[0]['purchase_order'], po.name)
|
||||
|
||||
self.assertEqual(po_data[0]['rm_item_code'], '_Test Item')
|
||||
self.assertEqual(po_data[0]['p_qty'], 8)
|
||||
self.assertEqual(po_data[0]['transferred_qty'], 2)
|
||||
|
||||
self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100')
|
||||
self.assertEqual(po_data[1]['p_qty'], 19)
|
||||
self.assertEqual(po_data[1]['transferred_qty'], 1)
|
||||
|
||||
se.cancel()
|
||||
po.cancel()
|
||||
|
||||
def transfer_subcontracted_raw_materials(po):
|
||||
# Order of supplied items fetched in PO is flaky
|
||||
transfer_qty_map = {
|
||||
'_Test Item': 2,
|
||||
'_Test Item Home Desktop 100': 1
|
||||
}
|
||||
|
||||
item_1 = po.supplied_items[0].rm_item_code
|
||||
item_2 = po.supplied_items[1].rm_item_code
|
||||
|
||||
rm_item = [
|
||||
{'item_code': '_Test Item', 'rm_item_code': '_Test Item', 'item_name': '_Test Item', 'qty': 1,
|
||||
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100, 'stock_uom': 'Nos'},
|
||||
{'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2,
|
||||
'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
|
||||
{
|
||||
'name': po.supplied_items[0].name,
|
||||
'item_code': item_1,
|
||||
'rm_item_code': item_1,
|
||||
'item_name': item_1,
|
||||
'qty': transfer_qty_map[item_1],
|
||||
'warehouse': '_Test Warehouse - _TC',
|
||||
'rate': 100,
|
||||
'amount': 100 * transfer_qty_map[item_1],
|
||||
'stock_uom': 'Nos'
|
||||
},
|
||||
{
|
||||
'name': po.supplied_items[1].name,
|
||||
'item_code': item_2,
|
||||
'rm_item_code': item_2,
|
||||
'item_name': item_2,
|
||||
'qty': transfer_qty_map[item_2],
|
||||
'warehouse': '_Test Warehouse - _TC',
|
||||
'rate': 100,
|
||||
'amount': 100 * transfer_qty_map[item_2],
|
||||
'stock_uom': 'Nos'
|
||||
}
|
||||
]
|
||||
rm_item_string = json.dumps(rm_item)
|
||||
se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
|
||||
se.from_warehouse = '_Test Warehouse 1 - _TC'
|
||||
se.to_warehouse = '_Test Warehouse 1 - _TC'
|
||||
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
|
||||
se.from_warehouse = '_Test Warehouse - _TC'
|
||||
se.to_warehouse = '_Test Warehouse - _TC'
|
||||
se.stock_entry_type = 'Send to Subcontractor'
|
||||
se.save()
|
||||
se.submit()
|
||||
return se
|
||||
|
@ -610,8 +610,8 @@ class AccountsController(TransactionBase):
|
||||
order_field = "purchase_order"
|
||||
order_doctype = "Purchase Order"
|
||||
|
||||
order_list = list(set([d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)]))
|
||||
order_list = list(set(d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
journal_entries = get_advance_journal_entries(party_type, party, party_account,
|
||||
amount_field, order_doctype, order_list, include_unallocated)
|
||||
@ -635,8 +635,8 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def validate_advance_entries(self):
|
||||
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
|
||||
order_list = list(set([d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)]))
|
||||
order_list = list(set(d.get(order_field)
|
||||
for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
if not order_list: return
|
||||
|
||||
@ -828,6 +828,12 @@ class AccountsController(TransactionBase):
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||
|
||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
||||
if self.doctype != "Purchase Invoice":
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
|
||||
self.throw_overbill_exception(item, max_allowed_amt)
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
|
||||
|
@ -11,16 +11,17 @@ 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
|
||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
|
||||
class BuyingController(StockController):
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.subcontracting import Subcontracting
|
||||
|
||||
class BuyingController(StockController, Subcontracting):
|
||||
|
||||
def get_feed(self):
|
||||
if self.get("supplier_name"):
|
||||
@ -57,6 +58,11 @@ class BuyingController(StockController):
|
||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||
self.update_valuation_rate()
|
||||
|
||||
def onload(self):
|
||||
super(BuyingController, self).onload()
|
||||
self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
|
||||
'backflush_raw_materials_of_subcontract_based_on'))
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
super(BuyingController, self).set_missing_values(for_validate)
|
||||
|
||||
@ -171,18 +177,19 @@ class BuyingController(StockController):
|
||||
|
||||
TODO: rename item_tax_amount to valuation_tax_amount
|
||||
"""
|
||||
stock_and_asset_items = []
|
||||
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
|
||||
|
||||
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
||||
last_item_idx = 1
|
||||
for d in self.get("items"):
|
||||
if d.item_code and d.item_code in stock_and_asset_items:
|
||||
if (d.item_code and d.item_code in stock_and_asset_items):
|
||||
stock_and_asset_items_qty += flt(d.qty)
|
||||
stock_and_asset_items_amount += flt(d.base_net_amount)
|
||||
last_item_idx = d.idx
|
||||
|
||||
total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
|
||||
if d.category in ["Valuation", "Valuation and Total"]])
|
||||
total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
|
||||
if d.category in ["Valuation", "Valuation and Total"])
|
||||
|
||||
valuation_amount_adjustment = total_valuation_amount
|
||||
for i, item in enumerate(self.get("items")):
|
||||
@ -255,7 +262,7 @@ class BuyingController(StockController):
|
||||
supplied_items_cost = 0.0
|
||||
for d in self.get("supplied_items"):
|
||||
if d.reference_name == item_row_id:
|
||||
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
|
||||
if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
|
||||
rate = get_incoming_rate({
|
||||
"item_code": d.rm_item_code,
|
||||
"warehouse": self.supplier_warehouse,
|
||||
@ -285,11 +292,13 @@ class BuyingController(StockController):
|
||||
if item in self.sub_contracted_items and not item.bom:
|
||||
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
|
||||
|
||||
if self.doctype == "Purchase Order":
|
||||
for supplied_item in self.get("supplied_items"):
|
||||
if not supplied_item.reserve_warehouse:
|
||||
frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
|
||||
if self.doctype != "Purchase Order":
|
||||
return
|
||||
|
||||
for row in self.get("supplied_items"):
|
||||
if not row.reserve_warehouse:
|
||||
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
|
||||
frappe.throw(_(msg))
|
||||
else:
|
||||
for item in self.get("items"):
|
||||
if item.bom:
|
||||
@ -297,23 +306,7 @@ class BuyingController(StockController):
|
||||
|
||||
def create_raw_materials_supplied(self, raw_material_table):
|
||||
if self.is_subcontracted=="Yes":
|
||||
parent_items = []
|
||||
backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
|
||||
"backflush_raw_materials_of_subcontract_based_on")
|
||||
if (self.doctype == 'Purchase Receipt' and
|
||||
backflush_raw_materials_based_on != 'BOM'):
|
||||
self.update_raw_materials_supplied_based_on_stock_entries()
|
||||
else:
|
||||
for item in self.get("items"):
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
item.rm_supp_cost = 0.0
|
||||
if item.bom and item.item_code in self.sub_contracted_items:
|
||||
self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
|
||||
|
||||
if [item.item_code, item.name] not in parent_items:
|
||||
parent_items.append([item.item_code, item.name])
|
||||
|
||||
self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
|
||||
self.set_materials_for_subcontracted_items(raw_material_table)
|
||||
|
||||
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
for item in self.get("items"):
|
||||
@ -322,176 +315,6 @@ class BuyingController(StockController):
|
||||
if self.is_subcontracted == "No" and self.get("supplied_items"):
|
||||
self.set('supplied_items', [])
|
||||
|
||||
def update_raw_materials_supplied_based_on_stock_entries(self):
|
||||
self.set('supplied_items', [])
|
||||
|
||||
purchase_orders = set([d.purchase_order for d in self.items])
|
||||
|
||||
# qty of raw materials backflushed (for each item per purchase order)
|
||||
backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
|
||||
|
||||
# qty of "finished good" item yet to be received
|
||||
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
|
||||
|
||||
# qty of raw materials transferred to the supplier
|
||||
transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
|
||||
|
||||
non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
|
||||
|
||||
item_key = '{}{}'.format(item.item_code, item.purchase_order)
|
||||
|
||||
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 = (raw_material.rm_item_code, item.item_code, item.purchase_order)
|
||||
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
||||
|
||||
consumed_qty = raw_material_data.get('qty', 0)
|
||||
consumed_serial_nos = raw_material_data.get('serial_no', '')
|
||||
consumed_batch_nos = raw_material_data.get('batch_nos', '')
|
||||
|
||||
transferred_qty = raw_material.qty
|
||||
|
||||
rm_qty_to_be_consumed = transferred_qty - consumed_qty
|
||||
|
||||
# backflush all remaining transferred qty in the last Purchase Receipt
|
||||
if fg_yet_to_be_received == item.qty:
|
||||
qty = rm_qty_to_be_consumed
|
||||
else:
|
||||
qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
|
||||
|
||||
if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
|
||||
qty = frappe.utils.ceil(qty)
|
||||
|
||||
if qty > rm_qty_to_be_consumed:
|
||||
qty = rm_qty_to_be_consumed
|
||||
|
||||
if not qty: continue
|
||||
|
||||
if raw_material.serial_nos:
|
||||
set_serial_nos(raw_material, consumed_serial_nos, qty)
|
||||
|
||||
if raw_material.batch_nos:
|
||||
backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
|
||||
|
||||
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, item.purchase_order)
|
||||
|
||||
for batch_data in batches_qty:
|
||||
qty = batch_data['qty']
|
||||
raw_material.batch_no = batch_data['batch']
|
||||
if qty > 0:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
else:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
|
||||
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
|
||||
rm = self.append('supplied_items', {})
|
||||
rm.update(raw_material_data)
|
||||
|
||||
if not rm.main_item_code:
|
||||
rm.main_item_code = fg_item_row.item_code
|
||||
|
||||
rm.reference_name = fg_item_row.name
|
||||
rm.required_qty = qty
|
||||
rm.consumed_qty = qty
|
||||
|
||||
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
|
||||
exploded_item = 1
|
||||
if hasattr(item, 'include_exploded_items'):
|
||||
exploded_item = item.get('include_exploded_items')
|
||||
|
||||
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
|
||||
|
||||
used_alternative_items = []
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
|
||||
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
|
||||
|
||||
raw_materials_cost = 0
|
||||
items = list(set([d.item_code for d in bom_items]))
|
||||
item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
|
||||
from `tabItem` i, `tabItem Default` id
|
||||
where id.parent=i.name and id.company=%s and i.name in ({0})"""
|
||||
.format(", ".join(["%s"] * len(items))), [self.company] + items))
|
||||
|
||||
for bom_item in bom_items:
|
||||
if self.doctype == "Purchase Order":
|
||||
reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
|
||||
if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
|
||||
reserve_warehouse = None
|
||||
|
||||
conversion_factor = item.conversion_factor
|
||||
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
|
||||
bom_item.item_code in used_alternative_items):
|
||||
alternative_item_data = used_alternative_items.get(bom_item.item_code)
|
||||
bom_item.item_code = alternative_item_data.item_code
|
||||
bom_item.item_name = alternative_item_data.item_name
|
||||
bom_item.stock_uom = alternative_item_data.stock_uom
|
||||
conversion_factor = alternative_item_data.conversion_factor
|
||||
bom_item.description = alternative_item_data.description
|
||||
|
||||
# check if exists
|
||||
exists = 0
|
||||
for d in self.get(raw_material_table):
|
||||
if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
|
||||
and d.reference_name == item.name:
|
||||
rm, exists = d, 1
|
||||
break
|
||||
|
||||
if not exists:
|
||||
rm = self.append(raw_material_table, {})
|
||||
|
||||
required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
|
||||
flt(conversion_factor), rm.precision("required_qty"))
|
||||
rm.reference_name = item.name
|
||||
rm.bom_detail_no = bom_item.name
|
||||
rm.main_item_code = item.item_code
|
||||
rm.rm_item_code = bom_item.item_code
|
||||
rm.stock_uom = bom_item.stock_uom
|
||||
rm.required_qty = required_qty
|
||||
rm.rate = bom_item.rate
|
||||
rm.conversion_factor = conversion_factor
|
||||
|
||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||
rm.consumed_qty = required_qty
|
||||
rm.description = bom_item.description
|
||||
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
|
||||
rm.batch_no = item.batch_no
|
||||
elif not rm.reserve_warehouse:
|
||||
rm.reserve_warehouse = reserve_warehouse
|
||||
|
||||
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
|
||||
"""Remove all those child items which are no longer present in main item table"""
|
||||
delete_list = []
|
||||
for d in self.get(raw_material_table):
|
||||
if [d.main_item_code, d.reference_name] not in parent_items:
|
||||
# mark for deletion from doclist
|
||||
delete_list.append(d)
|
||||
|
||||
# delete from doclist
|
||||
if delete_list:
|
||||
rm_supplied_details = self.get(raw_material_table)
|
||||
self.set(raw_material_table, [])
|
||||
for d in rm_supplied_details:
|
||||
if d not in delete_list:
|
||||
self.append(raw_material_table, d)
|
||||
|
||||
@property
|
||||
def sub_contracted_items(self):
|
||||
if not hasattr(self, "_sub_contracted_items"):
|
||||
@ -683,6 +506,7 @@ class BuyingController(StockController):
|
||||
self.process_fixed_asset()
|
||||
self.update_fixed_asset(field)
|
||||
|
||||
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
|
||||
update_last_purchase_rate(self, is_submit = 1)
|
||||
|
||||
def on_cancel(self):
|
||||
@ -691,7 +515,9 @@ class BuyingController(StockController):
|
||||
if self.get('is_return'):
|
||||
return
|
||||
|
||||
if self.doctype in ['Purchase Order', 'Purchase Receipt']:
|
||||
update_last_purchase_rate(self, is_submit = 0)
|
||||
|
||||
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
|
||||
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
|
||||
|
||||
@ -863,104 +689,6 @@ class BuyingController(StockController):
|
||||
else:
|
||||
validate_item_type(self, "is_purchase_item", "purchase")
|
||||
|
||||
|
||||
def get_items_from_bom(item_code, bom, exploded_item=1):
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
|
||||
bom_items = frappe.db.sql("""select t2.item_code, t2.name,
|
||||
t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
|
||||
t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
|
||||
from
|
||||
`tabBOM` t1, `tab{0}` t2, tabItem t3
|
||||
where
|
||||
t2.parent = t1.name and t1.item = %s
|
||||
and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
|
||||
and t2.sourced_by_supplier = 0
|
||||
and t2.item_code = t3.name""".format(doctype),
|
||||
(item_code, bom), as_dict=1)
|
||||
|
||||
if not bom_items:
|
||||
msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
|
||||
|
||||
return bom_items
|
||||
|
||||
def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
|
||||
common_query = """
|
||||
SELECT
|
||||
sed.item_code AS rm_item_code,
|
||||
SUM(sed.qty) AS qty,
|
||||
sed.description,
|
||||
sed.stock_uom,
|
||||
sed.subcontracted_item AS main_item_code,
|
||||
{serial_no_concat_syntax} AS serial_nos,
|
||||
{batch_no_concat_syntax} AS batch_nos
|
||||
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 IFNULL(sed.t_warehouse, '') != ''
|
||||
AND IFNULL(sed.subcontracted_item, '') in ('', %s)
|
||||
GROUP BY sed.item_code, sed.subcontracted_item
|
||||
"""
|
||||
raw_materials = frappe.db.multisql({
|
||||
'mariadb': common_query.format(
|
||||
serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
|
||||
batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
|
||||
),
|
||||
'postgres': common_query.format(
|
||||
serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
|
||||
batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
|
||||
)
|
||||
}, (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
return raw_materials
|
||||
|
||||
def get_backflushed_subcontracted_raw_materials(purchase_orders):
|
||||
purchase_receipts = frappe.get_all("Purchase Receipt Item",
|
||||
fields = ["purchase_order", "item_code", "name", "parent"],
|
||||
filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
|
||||
|
||||
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 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, data.main_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": [],
|
||||
"consumed_batch": {}
|
||||
}))
|
||||
|
||||
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))
|
||||
|
||||
if data.get("batch_no"):
|
||||
if data.get("batch_no") in row.consumed_batch:
|
||||
row.consumed_batch[data.get("batch_no")] += data.consumed_qty
|
||||
else:
|
||||
row.consumed_batch[data.get("batch_no")] = data.consumed_qty
|
||||
|
||||
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", "main_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"],
|
||||
@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
|
||||
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
|
||||
|
||||
frappe.throw(error_message)
|
||||
|
||||
def get_qty_to_be_received(purchase_orders):
|
||||
return frappe._dict(frappe.db.sql("""
|
||||
SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
|
||||
SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
|
||||
FROM `tabPurchase Order Item` poi
|
||||
WHERE
|
||||
poi.`parent` in %s
|
||||
GROUP BY poi.`item_code`, poi.`parent`
|
||||
HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
|
||||
""", (purchase_orders)))
|
||||
|
||||
def get_non_stock_items(purchase_order, fg_item_code):
|
||||
return frappe.db.sql("""
|
||||
SELECT
|
||||
pois.main_item_code,
|
||||
pois.rm_item_code,
|
||||
item.description,
|
||||
pois.required_qty AS qty,
|
||||
pois.rate,
|
||||
1 as non_stock_item,
|
||||
pois.stock_uom
|
||||
FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
|
||||
WHERE
|
||||
pois.`rm_item_code` = item.`name`
|
||||
AND item.is_stock_item = 0
|
||||
AND pois.`parent` = %s
|
||||
AND pois.`main_item_code` = %s
|
||||
""", (purchase_order, fg_item_code), as_dict=1)
|
||||
|
||||
|
||||
def set_serial_nos(raw_material, consumed_serial_nos, qty):
|
||||
serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
|
||||
set(get_serial_nos(consumed_serial_nos))
|
||||
if serial_nos and qty <= len(serial_nos):
|
||||
raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
|
||||
|
||||
def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
# returns
|
||||
# {
|
||||
# (item_code, fg_code): {
|
||||
# batch1: 10, # qty
|
||||
# batch2: 16
|
||||
# },
|
||||
# }
|
||||
transferred_batch_qty_map = {}
|
||||
transferred_batches = frappe.db.sql("""
|
||||
SELECT
|
||||
sed.batch_no,
|
||||
SUM(sed.qty) AS qty,
|
||||
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 ifnull(sed.subcontracted_item, '') in ('', %s)
|
||||
AND sed.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
sed.batch_no,
|
||||
sed.item_code
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in transferred_batches:
|
||||
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, OrderedDict())
|
||||
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return transferred_batch_qty_map
|
||||
|
||||
def get_backflushed_batch_qty_map(purchase_order, fg_item):
|
||||
# returns
|
||||
# {
|
||||
# (item_code, fg_code): {
|
||||
# batch1: 10, # qty
|
||||
# batch2: 16
|
||||
# },
|
||||
# }
|
||||
backflushed_batch_qty_map = {}
|
||||
backflushed_batches = frappe.db.sql("""
|
||||
SELECT
|
||||
pris.batch_no,
|
||||
SUM(pris.consumed_qty) AS qty,
|
||||
pris.rm_item_code AS item_code
|
||||
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
|
||||
WHERE
|
||||
pr.name = pri.parent
|
||||
AND pri.parent = pris.parent
|
||||
AND pri.purchase_order = %s
|
||||
AND pri.item_code = pris.main_item_code
|
||||
AND pr.docstatus = 1
|
||||
AND pris.main_item_code = %s
|
||||
AND pris.batch_no IS NOT NULL
|
||||
GROUP BY
|
||||
pris.rm_item_code, pris.batch_no
|
||||
""", (purchase_order, fg_item), as_dict=1)
|
||||
|
||||
for batch_data in backflushed_batches:
|
||||
backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
|
||||
backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return backflushed_batch_qty_map
|
||||
|
||||
def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, 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), {})
|
||||
|
||||
available_batches = []
|
||||
|
||||
for (batch, transferred_qty) in transferred_batches.items():
|
||||
backflushed_qty = backflushed_batches.get(batch, 0)
|
||||
available_qty = transferred_qty - backflushed_qty
|
||||
|
||||
if available_qty >= required_qty:
|
||||
available_batches.append({'batch': batch, 'qty': required_qty})
|
||||
break
|
||||
elif available_qty != 0:
|
||||
available_batches.append({'batch': batch, 'qty': available_qty})
|
||||
required_qty -= available_qty
|
||||
|
||||
for row in available_batches:
|
||||
if backflushed_batches.get(row.get('batch'), 0) > 0:
|
||||
backflushed_batches[row.get('batch')] += row.get('qty')
|
||||
else:
|
||||
backflushed_batches[row.get('batch')] = row.get('qty')
|
||||
|
||||
return available_batches
|
||||
|
@ -19,7 +19,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Employee", ["name", "employee_name"])
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabEmployee`
|
||||
where status = 'Active'
|
||||
where status in ('Active', 'Suspended')
|
||||
and docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or employee_name like %(txt)s)
|
||||
@ -88,7 +88,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
fields = get_fields("Customer", fields)
|
||||
|
||||
searchfields = frappe.get_meta("Customer").get_search_fields()
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
|
||||
|
||||
return frappe.db.sql("""select {fields} from `tabCustomer`
|
||||
where docstatus < 2
|
||||
@ -315,7 +315,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql("""select {fields} from `tabProject`
|
||||
where
|
||||
`tabProject`.status not in ("Completed", "Cancelled")
|
||||
and {cond} {match_cond} {scond}
|
||||
and {cond} {scond} {match_cond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
idx desc,
|
||||
|
@ -428,7 +428,7 @@ class SellingController(StockController):
|
||||
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
|
||||
|
||||
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
|
||||
doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
|
||||
doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)))
|
||||
if doc_list:
|
||||
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
|
||||
|
||||
|
@ -299,8 +299,8 @@ class StatusUpdater(Document):
|
||||
args['name'] = self.get(args['percent_join_field_parent'])
|
||||
self._update_percent_field(args, update_modified)
|
||||
else:
|
||||
distinct_transactions = set([d.get(args['percent_join_field'])
|
||||
for d in self.get_all_children(args['source_dt'])])
|
||||
distinct_transactions = set(d.get(args['percent_join_field'])
|
||||
for d in self.get_all_children(args['source_dt']))
|
||||
|
||||
for name in distinct_transactions:
|
||||
if name:
|
||||
|
@ -313,7 +313,7 @@ class StockController(AccountsController):
|
||||
|
||||
def get_serialized_items(self):
|
||||
serialized_items = []
|
||||
item_codes = list(set([d.item_code for d in self.get("items")]))
|
||||
item_codes = list(set(d.item_code for d in self.get("items")))
|
||||
if item_codes:
|
||||
serialized_items = frappe.db.sql_list("""select name from `tabItem`
|
||||
where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))),
|
||||
@ -324,8 +324,8 @@ class StockController(AccountsController):
|
||||
def validate_warehouse(self):
|
||||
from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
|
||||
|
||||
warehouses = list(set([d.warehouse for d in
|
||||
self.get("items") if getattr(d, "warehouse", None)]))
|
||||
warehouses = list(set(d.warehouse for d in
|
||||
self.get("items") if getattr(d, "warehouse", None)))
|
||||
|
||||
target_warehouses = list(set([d.target_warehouse for d in
|
||||
self.get("items") if getattr(d, "target_warehouse", None)]))
|
||||
@ -501,7 +501,6 @@ class StockController(AccountsController):
|
||||
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||
self.company, self.doctype, self.name)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_quality_inspections(doctype, docname, items):
|
||||
if isinstance(items, str):
|
||||
@ -533,21 +532,75 @@ def make_quality_inspections(doctype, docname, items):
|
||||
|
||||
return inspections
|
||||
|
||||
|
||||
def is_reposting_pending():
|
||||
return frappe.db.exists("Repost Item Valuation",
|
||||
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
||||
|
||||
def future_sle_exists(args, sl_entries=None):
|
||||
key = (args.voucher_type, args.voucher_no)
|
||||
|
||||
def future_sle_exists(args):
|
||||
sl_entries = frappe.get_all("Stock Ledger Entry",
|
||||
if validate_future_sle_not_exists(args, key, sl_entries):
|
||||
return False
|
||||
elif get_cached_data(args, key):
|
||||
return True
|
||||
|
||||
if not sl_entries:
|
||||
sl_entries = get_sle_entries_against_voucher(args)
|
||||
if not sl_entries:
|
||||
return
|
||||
|
||||
or_conditions = get_conditions_to_validate_future_sle(sl_entries)
|
||||
|
||||
data = frappe.db.sql("""
|
||||
select item_code, warehouse, count(name) as total_row
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
({})
|
||||
and timestamp(posting_date, posting_time)
|
||||
>= timestamp(%(posting_date)s, %(posting_time)s)
|
||||
and voucher_no != %(voucher_no)s
|
||||
and is_cancelled = 0
|
||||
GROUP BY
|
||||
item_code, warehouse
|
||||
""".format(" or ".join(or_conditions)), args, as_dict=1)
|
||||
|
||||
for d in data:
|
||||
frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
|
||||
|
||||
return len(data)
|
||||
|
||||
def validate_future_sle_not_exists(args, key, sl_entries=None):
|
||||
item_key = ''
|
||||
if args.get('item_code'):
|
||||
item_key = (args.get('item_code'), args.get('warehouse'))
|
||||
|
||||
if not sl_entries and hasattr(frappe.local, 'future_sle'):
|
||||
if (not frappe.local.future_sle.get(key) or
|
||||
(item_key and item_key not in frappe.local.future_sle.get(key))):
|
||||
return True
|
||||
|
||||
def get_cached_data(args, key):
|
||||
if not hasattr(frappe.local, 'future_sle'):
|
||||
frappe.local.future_sle = {}
|
||||
|
||||
if key not in frappe.local.future_sle:
|
||||
frappe.local.future_sle[key] = frappe._dict({})
|
||||
|
||||
if args.get('item_code'):
|
||||
item_key = (args.get('item_code'), args.get('warehouse'))
|
||||
count = frappe.local.future_sle[key].get(item_key)
|
||||
|
||||
return True if (count or count == 0) else False
|
||||
else:
|
||||
return frappe.local.future_sle[key]
|
||||
|
||||
def get_sle_entries_against_voucher(args):
|
||||
return frappe.get_all("Stock Ledger Entry",
|
||||
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
|
||||
fields=["item_code", "warehouse"],
|
||||
order_by="creation asc")
|
||||
|
||||
if not sl_entries:
|
||||
return
|
||||
|
||||
def get_conditions_to_validate_future_sle(sl_entries):
|
||||
warehouse_items_map = {}
|
||||
for entry in sl_entries:
|
||||
if entry.warehouse not in warehouse_items_map:
|
||||
@ -558,23 +611,10 @@ def future_sle_exists(args):
|
||||
or_conditions = []
|
||||
for warehouse, items in warehouse_items_map.items():
|
||||
or_conditions.append(
|
||||
"warehouse = '{}' and item_code in ({})".format(
|
||||
warehouse,
|
||||
", ".join(frappe.db.escape(item) for item in items)
|
||||
)
|
||||
)
|
||||
f"""warehouse = {frappe.db.escape(warehouse)}
|
||||
and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
|
||||
|
||||
return frappe.db.sql("""
|
||||
select name
|
||||
from `tabStock Ledger Entry`
|
||||
where
|
||||
({})
|
||||
and timestamp(posting_date, posting_time)
|
||||
>= timestamp(%(posting_date)s, %(posting_time)s)
|
||||
and voucher_no != %(voucher_no)s
|
||||
and is_cancelled = 0
|
||||
limit 1
|
||||
""".format(" or ".join(or_conditions)), args)
|
||||
return or_conditions
|
||||
|
||||
def create_repost_item_valuation_entry(args):
|
||||
args = frappe._dict(args)
|
||||
|
393
erpnext/controllers/subcontracting.py
Normal file
393
erpnext/controllers/subcontracting.py
Normal file
@ -0,0 +1,393 @@
|
||||
import frappe
|
||||
import copy
|
||||
from frappe import _
|
||||
from frappe.utils import flt, cint, get_link_to_form
|
||||
from collections import defaultdict
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
class Subcontracting():
|
||||
def set_materials_for_subcontracted_items(self, raw_material_table):
|
||||
if self.doctype == 'Purchase Invoice' and not self.update_stock:
|
||||
return
|
||||
|
||||
self.raw_material_table = raw_material_table
|
||||
self.__identify_change_in_item_table()
|
||||
self.__prepare_supplied_items()
|
||||
self.__validate_supplied_items()
|
||||
|
||||
def __prepare_supplied_items(self):
|
||||
self.initialized_fields()
|
||||
self.__get_purchase_orders()
|
||||
self.__get_pending_qty_to_receive()
|
||||
self.get_available_materials()
|
||||
self.__remove_changed_rows()
|
||||
self.__set_supplied_items()
|
||||
|
||||
def initialized_fields(self):
|
||||
self.available_materials = frappe._dict()
|
||||
self.__transferred_items = frappe._dict()
|
||||
self.alternative_item_details = frappe._dict()
|
||||
self.__get_backflush_based_on()
|
||||
|
||||
def __get_backflush_based_on(self):
|
||||
self.backflush_based_on = frappe.db.get_single_value("Buying Settings",
|
||||
"backflush_raw_materials_of_subcontract_based_on")
|
||||
|
||||
def __get_purchase_orders(self):
|
||||
self.purchase_orders = []
|
||||
|
||||
if self.doctype == 'Purchase Order':
|
||||
return
|
||||
|
||||
self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
|
||||
|
||||
def __identify_change_in_item_table(self):
|
||||
self.__changed_name = []
|
||||
self.__reference_name = []
|
||||
|
||||
if self.doctype == 'Purchase Order' or self.is_new():
|
||||
self.set(self.raw_material_table, [])
|
||||
return
|
||||
|
||||
item_dict = self.__get_data_before_save()
|
||||
if not item_dict:
|
||||
return True
|
||||
|
||||
for n_row in self.items:
|
||||
self.__reference_name.append(n_row.name)
|
||||
if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
|
||||
self.__changed_name.append(n_row.name)
|
||||
|
||||
if item_dict.get(n_row.name):
|
||||
del item_dict[n_row.name]
|
||||
|
||||
self.__changed_name.extend(item_dict.keys())
|
||||
|
||||
def __get_data_before_save(self):
|
||||
item_dict = {}
|
||||
if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save:
|
||||
for row in self._doc_before_save.get('items'):
|
||||
item_dict[row.name] = (row.item_code, row.qty)
|
||||
|
||||
return item_dict
|
||||
|
||||
def get_available_materials(self):
|
||||
''' Get the available raw materials which has been transferred to the supplier.
|
||||
available_materials = {
|
||||
(item_code, subcontracted_item, purchase_order): {
|
||||
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
|
||||
}
|
||||
}
|
||||
'''
|
||||
if not self.purchase_orders:
|
||||
return
|
||||
|
||||
for row in self.__get_transferred_items():
|
||||
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
|
||||
|
||||
if key not in self.available_materials:
|
||||
self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [],
|
||||
'batch_no': defaultdict(float), 'item_details': row, 'po_details': []})
|
||||
)
|
||||
|
||||
details = self.available_materials[key]
|
||||
details.qty += row.qty
|
||||
details.po_details.append(row.po_detail)
|
||||
|
||||
if row.serial_no:
|
||||
details.serial_no.extend(get_serial_nos(row.serial_no))
|
||||
|
||||
if row.batch_no:
|
||||
details.batch_no[row.batch_no] += row.qty
|
||||
|
||||
self.__set_alternative_item_details(row)
|
||||
|
||||
self.__transferred_items = copy.deepcopy(self.available_materials)
|
||||
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
|
||||
self.__update_consumed_materials(doctype)
|
||||
|
||||
def __update_consumed_materials(self, doctype, return_consumed_items=False):
|
||||
'''Deduct the consumed materials from the available materials.'''
|
||||
|
||||
pr_items = self.__get_received_items(doctype)
|
||||
if not pr_items:
|
||||
return ([], {}) if return_consumed_items else None
|
||||
|
||||
pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items}
|
||||
consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
|
||||
|
||||
if return_consumed_items:
|
||||
return (consumed_materials, pr_items)
|
||||
|
||||
for row in consumed_materials:
|
||||
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
|
||||
if not self.available_materials.get(key):
|
||||
continue
|
||||
|
||||
self.available_materials[key]['qty'] -= row.consumed_qty
|
||||
if row.serial_no:
|
||||
self.available_materials[key]['serial_no'] = list(
|
||||
set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no))
|
||||
)
|
||||
|
||||
if row.batch_no:
|
||||
self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty
|
||||
|
||||
def __get_transferred_items(self):
|
||||
fields = ['`tabStock Entry`.`purchase_order`']
|
||||
alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'}
|
||||
|
||||
child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount',
|
||||
'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor',
|
||||
's_warehouse', 't_warehouse', 'item_group', 'po_detail']
|
||||
|
||||
if self.backflush_based_on == 'BOM':
|
||||
child_table_fields.append('original_item')
|
||||
|
||||
for field in child_table_fields:
|
||||
fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}')
|
||||
|
||||
filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'],
|
||||
['Stock Entry', 'purchase_order', 'in', self.purchase_orders]]
|
||||
|
||||
return frappe.get_all('Stock Entry', fields = fields, filters=filters)
|
||||
|
||||
def __get_received_items(self, doctype):
|
||||
fields = []
|
||||
self.po_field = 'purchase_order'
|
||||
|
||||
for field in ['name', self.po_field, 'parent']:
|
||||
fields.append(f'`tab{doctype} Item`.`{field}`')
|
||||
|
||||
filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]]
|
||||
if doctype == 'Purchase Invoice':
|
||||
filters.append(['Purchase Invoice', 'update_stock', "=", 1])
|
||||
|
||||
return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
|
||||
|
||||
def __get_consumed_items(self, doctype, pr_items):
|
||||
return frappe.get_all('Purchase Receipt Item Supplied',
|
||||
fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'],
|
||||
filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype})
|
||||
|
||||
def __set_alternative_item_details(self, row):
|
||||
if row.get('original_item'):
|
||||
self.alternative_item_details[row.get('original_item')] = row
|
||||
|
||||
def __get_pending_qty_to_receive(self):
|
||||
'''Get qty to be received against the purchase order.'''
|
||||
|
||||
self.qty_to_be_received = defaultdict(float)
|
||||
|
||||
if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders:
|
||||
for row in frappe.get_all('Purchase Order Item',
|
||||
fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'],
|
||||
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}):
|
||||
|
||||
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item'
|
||||
fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit']
|
||||
|
||||
alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'}
|
||||
for field in ['item_code', 'name', 'rate', 'stock_uom',
|
||||
'source_warehouse', 'description', 'item_name', 'stock_uom']:
|
||||
fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}')
|
||||
|
||||
filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1],
|
||||
['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]]
|
||||
|
||||
return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or []
|
||||
|
||||
def __remove_changed_rows(self):
|
||||
if not self.__changed_name:
|
||||
return
|
||||
|
||||
i=1
|
||||
self.set(self.raw_material_table, [])
|
||||
for d in self._doc_before_save.supplied_items:
|
||||
if d.reference_name in self.__changed_name:
|
||||
continue
|
||||
|
||||
if (d.reference_name not in self.__reference_name):
|
||||
continue
|
||||
|
||||
d.idx = i
|
||||
self.append('supplied_items', d)
|
||||
|
||||
i += 1
|
||||
|
||||
def __set_supplied_items(self):
|
||||
self.bom_items = {}
|
||||
|
||||
has_supplied_items = True if self.get(self.raw_material_table) else False
|
||||
for row in self.items:
|
||||
if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name)
|
||||
or (has_supplied_items and not self.__changed_name))):
|
||||
continue
|
||||
|
||||
if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
|
||||
for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')):
|
||||
qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor)
|
||||
bom_item.main_item_code = row.item_code
|
||||
self.__update_reserve_warehouse(bom_item, row)
|
||||
self.__set_alternative_item(bom_item)
|
||||
self.__add_supplied_item(row, bom_item, qty)
|
||||
|
||||
elif self.backflush_based_on != 'BOM':
|
||||
for key, transfer_item in self.available_materials.items():
|
||||
if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
|
||||
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
|
||||
transfer_item.qty -= qty
|
||||
self.__add_supplied_item(row, transfer_item.get('item_details'), qty)
|
||||
|
||||
if self.qty_to_be_received:
|
||||
self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
|
||||
|
||||
def __update_reserve_warehouse(self, row, item):
|
||||
if self.doctype == 'Purchase Order':
|
||||
row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse)
|
||||
|
||||
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
|
||||
key = (item_row.item_code, item_row.purchase_order)
|
||||
|
||||
if self.qty_to_be_received == item_row.qty:
|
||||
return transfer_item.qty
|
||||
|
||||
if self.qty_to_be_received:
|
||||
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
|
||||
transfer_item.item_details.required_qty = transfer_item.qty
|
||||
|
||||
if (transfer_item.serial_no or frappe.get_cached_value('UOM',
|
||||
transfer_item.item_details.stock_uom, 'must_be_whole_number')):
|
||||
return frappe.utils.ceil(qty)
|
||||
|
||||
return qty
|
||||
|
||||
def __set_alternative_item(self, bom_item):
|
||||
if self.alternative_item_details.get(bom_item.rm_item_code):
|
||||
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
|
||||
|
||||
def __add_supplied_item(self, item_row, bom_item, qty):
|
||||
bom_item.conversion_factor = item_row.conversion_factor
|
||||
rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
rm_obj.reference_name = item_row.name
|
||||
|
||||
if self.doctype == 'Purchase Order':
|
||||
rm_obj.required_qty = qty
|
||||
else:
|
||||
rm_obj.consumed_qty = 0
|
||||
rm_obj.purchase_order = item_row.purchase_order
|
||||
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
|
||||
|
||||
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
|
||||
|
||||
if (self.available_materials.get(key) and self.available_materials[key]['batch_no']):
|
||||
new_rm_obj = None
|
||||
for batch_no, batch_qty in self.available_materials[key]['batch_no'].items():
|
||||
if batch_qty >= qty:
|
||||
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
|
||||
self.available_materials[key]['batch_no'][batch_no] -= qty
|
||||
return
|
||||
|
||||
elif qty > 0 and batch_qty > 0:
|
||||
qty -= batch_qty
|
||||
new_rm_obj = self.append(self.raw_material_table, bom_item)
|
||||
new_rm_obj.reference_name = item_row.name
|
||||
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
|
||||
self.available_materials[key]['batch_no'][batch_no] = 0
|
||||
|
||||
if abs(qty) > 0 and not new_rm_obj:
|
||||
self.__set_consumed_qty(rm_obj, qty)
|
||||
else:
|
||||
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
|
||||
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
|
||||
rm_obj.required_qty = required_qty
|
||||
rm_obj.consumed_qty = consumed_qty
|
||||
|
||||
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
|
||||
rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no,
|
||||
'required_qty': qty, 'purchase_order': item_row.purchase_order})
|
||||
|
||||
self.__set_serial_nos(item_row, rm_obj)
|
||||
|
||||
def __set_serial_nos(self, item_row, rm_obj):
|
||||
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
|
||||
if (self.available_materials.get(key) and self.available_materials[key]['serial_no']):
|
||||
used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)]
|
||||
rm_obj.serial_no = '\n'.join(used_serial_nos)
|
||||
|
||||
# Removed the used serial nos from the list
|
||||
for sn in used_serial_nos:
|
||||
self.available_materials[key]['serial_no'].remove(sn)
|
||||
|
||||
def set_consumed_qty_in_po(self):
|
||||
# Update consumed qty back in the purchase order
|
||||
if self.is_subcontracted != 'Yes':
|
||||
return
|
||||
|
||||
self.__get_purchase_orders()
|
||||
itemwise_consumed_qty = defaultdict(float)
|
||||
for doctype in ['Purchase Receipt', 'Purchase Invoice']:
|
||||
consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
|
||||
|
||||
for row in consumed_items:
|
||||
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
|
||||
itemwise_consumed_qty[key] += row.consumed_qty
|
||||
|
||||
self.__update_consumed_qty_in_po(itemwise_consumed_qty)
|
||||
|
||||
def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
|
||||
fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name']
|
||||
filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}
|
||||
|
||||
for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'):
|
||||
key = (row.rm_item_code, row.main_item_code, row.parent)
|
||||
consumed_qty = itemwise_consumed_qty.get(key, 0)
|
||||
|
||||
if row.supplied_qty < consumed_qty:
|
||||
consumed_qty = row.supplied_qty
|
||||
|
||||
itemwise_consumed_qty[key] -= consumed_qty
|
||||
frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty)
|
||||
|
||||
def __validate_supplied_items(self):
|
||||
if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']:
|
||||
return
|
||||
|
||||
for row in self.get(self.raw_material_table):
|
||||
self.__validate_consumed_qty(row)
|
||||
|
||||
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
|
||||
if not self.__transferred_items or not self.__transferred_items.get(key):
|
||||
return
|
||||
|
||||
self.__validate_batch_no(row, key)
|
||||
self.__validate_serial_no(row, key)
|
||||
|
||||
def __validate_consumed_qty(self, row):
|
||||
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
|
||||
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
|
||||
|
||||
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
|
||||
|
||||
def __validate_batch_no(self, row, key):
|
||||
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
|
||||
link = get_link_to_form('Purchase Order', row.purchase_order)
|
||||
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
|
||||
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
|
||||
|
||||
def __validate_serial_no(self, row, key):
|
||||
if row.get('serial_no'):
|
||||
serial_nos = get_serial_nos(row.get('serial_no'))
|
||||
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no'))
|
||||
|
||||
if incorrect_sn:
|
||||
incorrect_sn = "\n".join(incorrect_sn)
|
||||
link = get_link_to_form('Purchase Order', row.purchase_order)
|
||||
msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}'
|
||||
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
|
@ -378,10 +378,10 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
def manipulate_grand_total_for_inclusive_tax(self):
|
||||
# if fully inclusive taxes and diff
|
||||
if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]):
|
||||
if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
|
||||
last_tax = self.doc.get("taxes")[-1]
|
||||
non_inclusive_tax_amount = sum([flt(d.tax_amount_after_discount_amount)
|
||||
for d in self.doc.get("taxes") if not d.included_in_print_rate])
|
||||
non_inclusive_tax_amount = sum(flt(d.tax_amount_after_discount_amount)
|
||||
for d in self.doc.get("taxes") if not d.included_in_print_rate)
|
||||
|
||||
diff = self.doc.total + non_inclusive_tax_amount \
|
||||
- flt(last_tax.total, last_tax.precision("total"))
|
||||
@ -521,8 +521,8 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
def calculate_total_advance(self):
|
||||
if self.doc.docstatus < 2:
|
||||
total_allocated_amount = sum([flt(adv.allocated_amount, adv.precision("allocated_amount"))
|
||||
for adv in self.doc.get("advances")])
|
||||
total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount"))
|
||||
for adv in self.doc.get("advances"))
|
||||
|
||||
self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance"))
|
||||
|
||||
@ -622,7 +622,7 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
if self.doc.doctype == "Sales Invoice" \
|
||||
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \
|
||||
and any([d.type == "Cash" for d in self.doc.payments]):
|
||||
and any(d.type == "Cash" for d in self.doc.payments):
|
||||
grand_total = self.doc.rounded_total or self.doc.grand_total
|
||||
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
|
||||
|
||||
@ -658,7 +658,13 @@ class calculate_taxes_and_totals(object):
|
||||
item.margin_type = None
|
||||
item.margin_rate_or_amount = 0.0
|
||||
|
||||
if item.margin_type and item.margin_rate_or_amount:
|
||||
if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
|
||||
item.margin_type = "Amount"
|
||||
item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
|
||||
item.precision("margin_rate_or_amount"))
|
||||
item.rate_with_margin = item.rate
|
||||
|
||||
elif item.margin_type and item.margin_rate_or_amount:
|
||||
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
|
||||
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
|
||||
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
|
||||
|
@ -26,8 +26,8 @@ class TestMapper(unittest.TestCase):
|
||||
|
||||
# Assert that all inserted items are present in updated sales order
|
||||
src_items = item_list_1 + item_list_2 + item_list_3
|
||||
self.assertEqual(set([d for d in src_items]),
|
||||
set([d.item_code for d in updated_so.items]))
|
||||
self.assertEqual(set(d for d in src_items),
|
||||
set(d.item_code for d in updated_so.items))
|
||||
|
||||
|
||||
def make_quotation(self, item_list, customer):
|
||||
|
@ -113,7 +113,7 @@ def post_process(doctype, data):
|
||||
doc.set_indicator()
|
||||
|
||||
doc.status_display = ", ".join(doc.status_display)
|
||||
doc.items_preview = ", ".join([d.item_name for d in doc.items if d.item_name])
|
||||
doc.items_preview = ", ".join(d.item_name for d in doc.items if d.item_name)
|
||||
result.append(doc)
|
||||
|
||||
return result
|
||||
|
@ -22,10 +22,10 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
get_chart_data: function (_columns, result) {
|
||||
return {
|
||||
data: {
|
||||
labels: result.map(d => d[0]),
|
||||
labels: result.map(d => d.creation_date),
|
||||
datasets: [{
|
||||
name: "First Response Time",
|
||||
values: result.map(d => d[1])
|
||||
values: result.map(d => d.first_response_time)
|
||||
}]
|
||||
},
|
||||
type: "line",
|
||||
@ -35,8 +35,7 @@ frappe.query_reports["First Response Time for Opportunity"] = {
|
||||
hide_days: 0,
|
||||
hide_seconds: 0
|
||||
};
|
||||
value = frappe.utils.get_formatted_duration(d, duration_options);
|
||||
return value;
|
||||
return frappe.utils.get_formatted_duration(d, duration_options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ frappe.ui.form.on('Assessment Result', {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.trigger('setup_chart');
|
||||
}
|
||||
frm.set_df_property('details', 'read_only', 1);
|
||||
|
||||
frm.get_field('details').grid.cannot_add_rows = true;
|
||||
|
||||
frm.set_query('course', function() {
|
||||
return {
|
||||
|
@ -33,6 +33,7 @@ class Patient(Document):
|
||||
self.reload() # self.notify_update()
|
||||
|
||||
def on_update(self):
|
||||
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
|
||||
if self.customer:
|
||||
customer = frappe.get_doc('Customer', self.customer)
|
||||
if self.customer_group:
|
||||
@ -47,7 +48,6 @@ class Patient(Document):
|
||||
customer.ignore_mandatory = True
|
||||
customer.save(ignore_permissions=True)
|
||||
else:
|
||||
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
|
||||
create_customer(self)
|
||||
|
||||
def set_full_name(self):
|
||||
|
@ -207,7 +207,7 @@
|
||||
"label": "Status",
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Active\nInactive\nLeft",
|
||||
"options": "Active\nInactive\nSuspended\nLeft",
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
@ -813,7 +813,7 @@
|
||||
"idx": 24,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-06-12 11:31:37.730760",
|
||||
"modified": "2021-06-17 11:31:37.730760",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee",
|
||||
|
@ -4,7 +4,7 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr
|
||||
from frappe.utils import getdate, validate_email_address, today, add_years, cstr
|
||||
from frappe.model.naming import set_name_by_naming_series
|
||||
from frappe import throw, _, scrub
|
||||
from frappe.permissions import add_user_permission, remove_user_permission, \
|
||||
@ -12,7 +12,6 @@ from frappe.permissions import add_user_permission, remove_user_permission, \
|
||||
from frappe.model.document import Document
|
||||
from erpnext.utilities.transaction_base import delete_events
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
from erpnext.hr.doctype.job_offer.job_offer import get_staffing_plan_detail
|
||||
|
||||
class EmployeeUserDisabledError(frappe.ValidationError): pass
|
||||
class EmployeeLeftValidationError(frappe.ValidationError): pass
|
||||
@ -37,7 +36,7 @@ class Employee(NestedSet):
|
||||
|
||||
def validate(self):
|
||||
from erpnext.controllers.status_updater import validate_status
|
||||
validate_status(self.status, ["Active", "Inactive", "Left"])
|
||||
validate_status(self.status, ["Active", "Inactive", "Suspended", "Left"])
|
||||
|
||||
self.employee = self.name
|
||||
self.set_employee_name()
|
||||
|
@ -7,7 +7,8 @@ def get_data():
|
||||
'heatmap_message': _('This is based on the attendance of this Employee'),
|
||||
'fieldname': 'employee',
|
||||
'non_standard_fieldnames': {
|
||||
'Bank Account': 'party'
|
||||
'Bank Account': 'party',
|
||||
'Employee Grievance': 'raised_by'
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
@ -20,7 +21,7 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('Lifecycle'),
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation']
|
||||
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
|
||||
},
|
||||
{
|
||||
'label': _('Shift'),
|
||||
|
@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
|
||||
filters: [["status","=", "Active"]],
|
||||
get_indicator: function(doc) {
|
||||
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
|
||||
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status];
|
||||
indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray", "Suspended": "orange"}[doc.status];
|
||||
return indicator;
|
||||
}
|
||||
};
|
||||
|
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
0
erpnext/hr/doctype/employee_grievance/__init__.py
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
39
erpnext/hr/doctype/employee_grievance/employee_grievance.js
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Employee Grievance', {
|
||||
setup: function(frm) {
|
||||
frm.set_query('grievance_against_party', function() {
|
||||
return {
|
||||
filters: {
|
||||
name: ['in', [
|
||||
'Company', 'Department', 'Employee Group', 'Employee Grade', 'Employee']
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('associated_document_type', function() {
|
||||
let ignore_modules = ["Setup", "Core", "Integrations", "Automation", "Website",
|
||||
"Utilities", "Event Streaming", "Social", "Chat", "Data Migration", "Printing", "Desk", "Custom"];
|
||||
return {
|
||||
filters: {
|
||||
istable: 0,
|
||||
issingle: 0,
|
||||
module: ["Not In", ignore_modules]
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
grievance_against_party: function(frm) {
|
||||
let filters = {};
|
||||
if (frm.doc.grievance_against_party == 'Employee' && frm.doc.raised_by) {
|
||||
filters.name = ["!=", frm.doc.raised_by];
|
||||
}
|
||||
frm.set_query('grievance_against', function() {
|
||||
return {
|
||||
filters: filters
|
||||
};
|
||||
});
|
||||
},
|
||||
});
|
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
261
erpnext/hr/doctype/employee_grievance/employee_grievance.json
Normal file
@ -0,0 +1,261 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "HR-GRIEV-.YYYY.-.#####",
|
||||
"creation": "2021-05-11 13:41:51.485295",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"subject",
|
||||
"raised_by",
|
||||
"employee_name",
|
||||
"designation",
|
||||
"column_break_3",
|
||||
"date",
|
||||
"status",
|
||||
"reports_to",
|
||||
"grievance_details_section",
|
||||
"grievance_against_party",
|
||||
"grievance_against",
|
||||
"grievance_type",
|
||||
"column_break_11",
|
||||
"associated_document_type",
|
||||
"associated_document",
|
||||
"section_break_14",
|
||||
"description",
|
||||
"investigation_details_section",
|
||||
"cause_of_grievance",
|
||||
"resolution_details_section",
|
||||
"resolved_by",
|
||||
"resolution_date",
|
||||
"employee_responsible",
|
||||
"column_break_16",
|
||||
"resolution_detail",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "grievance_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Grievance Type",
|
||||
"options": "Grievance Type",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Date ",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Open",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Open\nInvestigated\nResolved\nInvalid",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "cause_of_grievance",
|
||||
"fieldtype": "Text",
|
||||
"label": "Cause of Grievance",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Investigated\" || doc.status == \"Resolved\""
|
||||
},
|
||||
{
|
||||
"fieldname": "resolution_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Resolution Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "resolved_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Resolved By",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Resolved\"",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee_responsible",
|
||||
"fieldtype": "Link",
|
||||
"label": "Employee Responsible ",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"fieldname": "resolution_detail",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Resolution Details",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "resolution_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Resolution Date",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Resolved\""
|
||||
},
|
||||
{
|
||||
"fieldname": "grievance_against",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Grievance Against",
|
||||
"options": "grievance_against_party",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "raised_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Raised By",
|
||||
"options": "Employee",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Employee Grievance",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "raised_by.designation",
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Link",
|
||||
"label": "Designation",
|
||||
"options": "Designation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "raised_by.reports_to",
|
||||
"fieldname": "reports_to",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reports To",
|
||||
"options": "Employee",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "grievance_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Grievance Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "grievance_against_party",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Grievance Against Party",
|
||||
"options": "DocType",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "associated_document_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Associated Document Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "associated_document",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Associated Document",
|
||||
"options": "associated_document_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "investigation_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Investigation Details"
|
||||
},
|
||||
{
|
||||
"fetch_from": "raised_by.employee_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Employee Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-21 12:51:01.499486",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Grievance",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 1,
|
||||
"cancel": 1,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"select": 1,
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "subject,raised_by,grievance_against_party",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "subject",
|
||||
"track_changes": 1
|
||||
}
|
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
15
erpnext/hr/doctype/employee_grievance/employee_grievance.py
Normal file
@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
|
||||
class EmployeeGrievance(Document):
|
||||
def on_submit(self):
|
||||
if self.status not in ["Invalid", "Resolved"]:
|
||||
frappe.throw(_("Only Employee Grievance with status {0} or {1} can be submitted").format(
|
||||
bold("Invalid"),
|
||||
bold("Resolved"))
|
||||
)
|
||||
|
@ -0,0 +1,12 @@
|
||||
frappe.listview_settings["Employee Grievance"] = {
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function(doc) {
|
||||
var colors = {
|
||||
"Open": "red",
|
||||
"Investigated": "orange",
|
||||
"Resolved": "green",
|
||||
"Invalid": "grey"
|
||||
};
|
||||
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
|
||||
}
|
||||
};
|
@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
from frappe.utils import today
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
class TestEmployeeGrievance(unittest.TestCase):
|
||||
def test_create_employee_grievance(self):
|
||||
create_employee_grievance()
|
||||
|
||||
def create_employee_grievance():
|
||||
grievance_type = create_grievance_type()
|
||||
emp_1 = make_employee("test_emp_grievance_@example.com", company="_Test Company")
|
||||
emp_2 = make_employee("testculprit@example.com", company="_Test Company")
|
||||
|
||||
grievance = frappe.new_doc("Employee Grievance")
|
||||
grievance.subject = "Test Employee Grievance"
|
||||
grievance.raised_by = emp_1
|
||||
grievance.date = today()
|
||||
grievance.grievance_type = grievance_type
|
||||
grievance.grievance_against_party = "Employee"
|
||||
grievance.grievance_against = emp_2
|
||||
grievance.description = "test descrip"
|
||||
|
||||
#set cause
|
||||
grievance.cause_of_grievance = "test cause"
|
||||
|
||||
#resolution details
|
||||
grievance.resolution_date = today()
|
||||
grievance.resolution_detail = "test resolution detail"
|
||||
grievance.resolved_by = "test_emp_grievance_@example.com"
|
||||
grievance.employee_responsible = emp_2
|
||||
grievance.status = "Resolved"
|
||||
|
||||
grievance.save()
|
||||
grievance.submit()
|
||||
|
||||
return grievance
|
||||
|
||||
|
||||
def create_grievance_type():
|
||||
if frappe.db.exists("Grievance Type", "Employee Abuse"):
|
||||
return frappe.get_doc("Grievance Type", "Employee Abuse")
|
||||
grievance_type = frappe.new_doc("Grievance Type")
|
||||
grievance_type.name = "Employee Abuse"
|
||||
grievance_type.description = "Test"
|
||||
grievance_type.save()
|
||||
|
||||
return grievance_type.name
|
||||
|
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
0
erpnext/hr/doctype/grievance_type/__init__.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.js
Normal file
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Grievance Type', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
70
erpnext/hr/doctype/grievance_type/grievance_type.json
Normal file
@ -0,0 +1,70 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "Prompt",
|
||||
"creation": "2021-05-11 12:41:50.256071",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break_5",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text",
|
||||
"label": "Description"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-21 12:54:37.764712",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Grievance Type",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/grievance_type.py
Normal file
@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class GrievanceType(Document):
|
||||
pass
|
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
8
erpnext/hr/doctype/grievance_type/test_grievance_type.py
Normal file
@ -0,0 +1,8 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestGrievanceType(unittest.TestCase):
|
||||
pass
|
@ -2,7 +2,7 @@
|
||||
// MIT License. See license.txt
|
||||
|
||||
frappe.listview_settings['Job Applicant'] = {
|
||||
add_fields: ["company", "designation", "job_applicant", "status"],
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Accepted") {
|
||||
return [__(doc.status), "green", "status,=," + doc.status];
|
||||
|
@ -110,6 +110,7 @@
|
||||
"label": "Allocation"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"bold": 1,
|
||||
"fieldname": "new_leaves_allocated",
|
||||
"fieldtype": "Float",
|
||||
@ -235,7 +236,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-14 15:28:26.335104",
|
||||
"modified": "2021-06-03 15:28:26.335104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Allocation",
|
||||
|
@ -8,6 +8,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from erpnext.hr.utils import set_employee_name, get_leave_period
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import expire_allocation, create_leave_ledger_entry
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_approved_leaves_for_period
|
||||
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class BackDatedAllocationError(frappe.ValidationError): pass
|
||||
@ -55,6 +56,43 @@ class LeaveAllocation(Document):
|
||||
if self.carry_forward:
|
||||
self.set_carry_forwarded_leaves_in_previous_allocation(on_cancel=True)
|
||||
|
||||
def on_update_after_submit(self):
|
||||
if self.has_value_changed("new_leaves_allocated"):
|
||||
self.validate_against_leave_applications()
|
||||
leaves_to_be_added = self.new_leaves_allocated - self.get_existing_leave_count()
|
||||
args = {
|
||||
"leaves": leaves_to_be_added,
|
||||
"from_date": self.from_date,
|
||||
"to_date": self.to_date,
|
||||
"is_carry_forward": 0
|
||||
}
|
||||
create_leave_ledger_entry(self, args, True)
|
||||
|
||||
def get_existing_leave_count(self):
|
||||
ledger_entries = frappe.get_all("Leave Ledger Entry",
|
||||
filters={
|
||||
"transaction_type": "Leave Allocation",
|
||||
"transaction_name": self.name,
|
||||
"employee": self.employee,
|
||||
"company": self.company,
|
||||
"leave_type": self.leave_type
|
||||
},
|
||||
pluck="leaves")
|
||||
total_existing_leaves = 0
|
||||
for entry in ledger_entries:
|
||||
total_existing_leaves += entry
|
||||
|
||||
return total_existing_leaves
|
||||
|
||||
def validate_against_leave_applications(self):
|
||||
leaves_taken = get_approved_leaves_for_period(self.employee, self.leave_type,
|
||||
self.from_date, self.to_date)
|
||||
if flt(leaves_taken) > flt(self.total_leaves_allocated):
|
||||
if frappe.db.get_value("Leave Type", self.leave_type, "allow_negative"):
|
||||
frappe.msgprint(_("Note: Total allocated leaves {0} shouldn't be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken))
|
||||
else:
|
||||
frappe.throw(_("Total allocated leaves {0} cannot be less than already approved leaves {1} for the period").format(self.total_leaves_allocated, leaves_taken), LessAllocationError)
|
||||
|
||||
def update_leave_policy_assignments_when_no_allocations_left(self):
|
||||
allocations = frappe.db.get_list("Leave Allocation", filters = {
|
||||
"docstatus": 1,
|
||||
|
@ -1,5 +1,6 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import erpnext
|
||||
import unittest
|
||||
from frappe.utils import nowdate, add_months, getdate, add_days
|
||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||
@ -164,6 +165,51 @@ class TestLeaveAllocation(unittest.TestCase):
|
||||
leave_allocation.cancel()
|
||||
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
|
||||
|
||||
def test_leave_addition_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
leave_allocation.new_leaves_allocated = 40
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
||||
|
||||
def test_leave_subtraction_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
leave_allocation.new_leaves_allocated = 10
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
||||
|
||||
def test_against_leave_application_validation_after_submit(self):
|
||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
||||
|
||||
leave_allocation = create_leave_allocation()
|
||||
leave_allocation.submit()
|
||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||
leave_application = frappe.get_doc({
|
||||
"doctype": 'Leave Application',
|
||||
"employee": employee.name,
|
||||
"leave_type": "_Test Leave Type",
|
||||
"from_date": add_months(nowdate(), 2),
|
||||
"to_date": add_months(add_days(nowdate(), 10), 2),
|
||||
"company": erpnext.get_default_company() or "_Test Company",
|
||||
"docstatus": 1,
|
||||
"status": "Approved",
|
||||
"leave_approver": 'test@example.com'
|
||||
})
|
||||
leave_application.submit()
|
||||
leave_allocation.new_leaves_allocated = 8
|
||||
leave_allocation.total_leaves_allocated = 8
|
||||
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
|
||||
|
||||
def create_leave_allocation(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
@ -41,7 +41,7 @@ class StaffingPlan(Document):
|
||||
|
||||
detail.total_estimated_cost = 0
|
||||
if detail.number_of_positions > 0:
|
||||
if detail.vacancies > 0 and detail.estimated_cost_per_position:
|
||||
if detail.vacancies and detail.estimated_cost_per_position:
|
||||
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
|
||||
|
||||
self.total_estimated_budget += detail.total_estimated_cost
|
||||
@ -76,12 +76,12 @@ class StaffingPlan(Document):
|
||||
if cint(staffing_plan_detail.vacancies) > cint(parent_plan_details[0].vacancies) or \
|
||||
flt(staffing_plan_detail.total_estimated_cost) > flt(parent_plan_details[0].total_estimated_cost):
|
||||
frappe.throw(_("You can only plan for upto {0} vacancies and budget {1} \
|
||||
for {2} as per staffing plan {3} for parent company {4}."
|
||||
.format(cint(parent_plan_details[0].vacancies),
|
||||
for {2} as per staffing plan {3} for parent company {4}.").format(
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_plan_details[0].name,
|
||||
parent_company)), ParentCompanyError)
|
||||
parent_company), ParentCompanyError)
|
||||
|
||||
#Get vacanices already planned for all companies down the hierarchy of Parent Company
|
||||
lft, rgt = frappe.get_cached_value('Company', parent_company, ["lft", "rgt"])
|
||||
@ -98,14 +98,14 @@ class StaffingPlan(Document):
|
||||
(flt(parent_plan_details[0].total_estimated_cost) < \
|
||||
(flt(staffing_plan_detail.total_estimated_cost) + flt(all_sibling_details.total_estimated_cost))):
|
||||
frappe.throw(_("{0} vacancies and {1} budget for {2} already planned for subsidiary companies of {3}. \
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}."
|
||||
.format(cint(all_sibling_details.vacancies),
|
||||
You can only plan for upto {4} vacancies and and budget {5} as per staffing plan {6} for parent company {3}.").format(
|
||||
cint(all_sibling_details.vacancies),
|
||||
all_sibling_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation),
|
||||
parent_company,
|
||||
cint(parent_plan_details[0].vacancies),
|
||||
parent_plan_details[0].total_estimated_cost,
|
||||
parent_plan_details[0].name)))
|
||||
parent_plan_details[0].name))
|
||||
|
||||
def validate_with_subsidiary_plans(self, staffing_plan_detail):
|
||||
#Valdate this plan with all child company plan
|
||||
@ -121,11 +121,11 @@ class StaffingPlan(Document):
|
||||
cint(staffing_plan_detail.vacancies) < cint(children_details.vacancies) or \
|
||||
flt(staffing_plan_detail.total_estimated_cost) < flt(children_details.total_estimated_cost):
|
||||
frappe.throw(_("Subsidiary companies have already planned for {1} vacancies at a budget of {2}. \
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies"
|
||||
.format(self.company,
|
||||
Staffing Plan for {0} should allocate more vacancies and budget for {3} than planned for its subsidiary companies").format(
|
||||
self.company,
|
||||
cint(children_details.vacancies),
|
||||
children_details.total_estimated_cost,
|
||||
frappe.bold(staffing_plan_detail.designation))), SubsidiaryCompanyError)
|
||||
frappe.bold(staffing_plan_detail.designation)), SubsidiaryCompanyError)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_designation_counts(designation, company):
|
||||
|
@ -11,8 +11,8 @@
|
||||
"event": "Submit",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>{{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n {% endif %}\n <li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>Note: This Training Event is mandatory</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
|
||||
"modified": "2021-05-24 16:29:13.165930",
|
||||
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>\n {{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b></li>\n {% endif %}\n <li>{{ _(\"Event Link\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>{{ _(\"Note: This Training Event is mandatory\") }}</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
|
||||
"modified": "2021-06-16 14:08:12.933367",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Scheduled",
|
||||
|
@ -29,14 +29,14 @@
|
||||
{{_("Timing")}}: <b>{{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }}</b>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
|
||||
</li>
|
||||
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
|
||||
<li>
|
||||
{{_("Start Time")}}: <b>{{ start.strftime("%A, %d %b %Y at %I:%M %p") }}</b>
|
||||
</li>
|
||||
<li>{{_("End Time")}}: <b>{{ end.strftime("%A, %d %b %Y at %I:%M %p") }}</b></li>
|
||||
{% endif %}
|
||||
<li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
|
||||
<li>{{ _("Event Link") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
|
||||
{% if doc.is_mandatory %}
|
||||
<li>Note: This Training Event is mandatory</li>
|
||||
<li>{{ _("Note: This Training Event is mandatory") }}</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -178,7 +178,7 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
|
||||
is_carry_forward, is_expired
|
||||
FROM `tabLeave Ledger Entry`
|
||||
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
|
||||
AND docstatus=1 AND leaves>0
|
||||
AND docstatus=1
|
||||
AND (from_date between %(from_date)s AND %(to_date)s
|
||||
OR to_date between %(from_date)s AND %(to_date)s
|
||||
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
||||
|
@ -153,6 +153,24 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Grievance Type",
|
||||
"link_to": "Grievance Type",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Employee Grievance",
|
||||
"link_to": "Employee Grievance",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Employee",
|
||||
"hidden": 0,
|
||||
@ -823,7 +841,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-04-26 13:36:15.413819",
|
||||
"modified": "2021-05-13 17:19:40.524444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR",
|
||||
|
@ -60,8 +60,9 @@ class Loan(AccountsController):
|
||||
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
|
||||
|
||||
def check_sanctioned_amount_limit(self):
|
||||
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
|
||||
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
|
||||
if sanctioned_amount_limit:
|
||||
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
|
||||
|
||||
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
|
||||
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
|
||||
@ -155,9 +156,29 @@ def update_total_amount_paid(doc):
|
||||
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
|
||||
|
||||
def get_total_loan_amount(applicant_type, applicant, company):
|
||||
return frappe.db.get_value('Loan',
|
||||
{'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1},
|
||||
'sum(loan_amount)')
|
||||
pending_amount = 0
|
||||
loan_details = frappe.db.get_all("Loan",
|
||||
filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1,
|
||||
"status": ("!=", "Closed")},
|
||||
fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid",
|
||||
"written_off_amount"])
|
||||
|
||||
interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type,
|
||||
"company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)"))
|
||||
|
||||
for loan in loan_details:
|
||||
if loan.status in ("Disbursed", "Loan Closure Requested"):
|
||||
pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \
|
||||
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
|
||||
elif loan.status == "Partially Disbursed":
|
||||
pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
|
||||
- flt(loan.total_principal_paid) - flt(loan.written_off_amount)
|
||||
elif loan.status == "Sanctioned":
|
||||
pending_amount += flt(loan.total_payment)
|
||||
|
||||
pending_amount += interest_amount
|
||||
|
||||
return pending_amount
|
||||
|
||||
def get_sanctioned_amount_limit(applicant_type, applicant, company):
|
||||
return frappe.db.get_value('Sanctioned Loan Amount',
|
||||
|
@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase):
|
||||
if not frappe.db.exists("Customer", "_Test Loan Customer"):
|
||||
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
|
||||
|
||||
self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
|
||||
if not frappe.db.exists("Customer", "_Test Loan Customer 1"):
|
||||
frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True)
|
||||
|
||||
self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name")
|
||||
self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name")
|
||||
|
||||
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
|
||||
|
||||
@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase):
|
||||
self.assertTrue(gl_entries1)
|
||||
self.assertTrue(gl_entries2)
|
||||
|
||||
def test_sanctioned_amount_limit(self):
|
||||
# Clear loan docs before checking
|
||||
frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'")
|
||||
frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'")
|
||||
frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'")
|
||||
|
||||
if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer",
|
||||
"applicant": "_Test Loan Customer 1", "company": "_Test Company"}):
|
||||
frappe.get_doc({
|
||||
"doctype": "Sanctioned Loan Amount",
|
||||
"applicant_type": "Customer",
|
||||
"applicant": "_Test Loan Customer 1",
|
||||
"sanctioned_amount_limit": 1500000,
|
||||
"company": "_Test Company"
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
# Make First Loan
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
"qty": 4000.00
|
||||
}]
|
||||
|
||||
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge)
|
||||
create_pledge(loan_application)
|
||||
loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01')
|
||||
loan.submit()
|
||||
|
||||
# Make second loan greater than the sanctioned amount
|
||||
loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge,
|
||||
do_not_save=True)
|
||||
self.assertRaises(frappe.ValidationError, loan_application.save)
|
||||
|
||||
def test_regular_loan_repayment(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase):
|
||||
unpledge_request.load_from_db()
|
||||
self.assertEqual(unpledge_request.docstatus, 1)
|
||||
|
||||
def test_santined_loan_security_unpledge(self):
|
||||
def test_sanctioned_loan_security_unpledge(self):
|
||||
pledge = [{
|
||||
"loan_security": "Test Security 1",
|
||||
"qty": 4000.00
|
||||
@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount):
|
||||
return lr
|
||||
|
||||
def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None,
|
||||
repayment_periods=None, posting_date=None):
|
||||
repayment_periods=None, posting_date=None, do_not_save=False):
|
||||
loan_application = frappe.new_doc('Loan Application')
|
||||
loan_application.applicant_type = 'Customer'
|
||||
loan_application.company = company
|
||||
@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
|
||||
for pledge in proposed_pledges:
|
||||
loan_application.append('proposed_pledges', pledge)
|
||||
|
||||
if do_not_save:
|
||||
return loan_application
|
||||
|
||||
loan_application.save()
|
||||
loan_application.submit()
|
||||
|
||||
|
@ -46,9 +46,11 @@ class LoanApplication(Document):
|
||||
frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
|
||||
|
||||
def check_sanctioned_amount_limit(self):
|
||||
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
|
||||
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
|
||||
|
||||
if sanctioned_amount_limit:
|
||||
total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
|
||||
|
||||
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
|
||||
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
|
||||
|
||||
|
@ -235,6 +235,7 @@ class LoanRepayment(AccountsController):
|
||||
else:
|
||||
remarks = _("Repayment against Loan: ") + self.against_loan
|
||||
|
||||
if not loan_details.repay_from_salary:
|
||||
if self.total_penalty_paid:
|
||||
gle_map.append(
|
||||
self.get_gl_dict({
|
||||
|
@ -71,7 +71,6 @@ frappe.ui.form.on("BOM", {
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.toggle_enable("item", frm.doc.__islocal);
|
||||
toggle_operations(frm);
|
||||
|
||||
frm.set_indicator_formatter('item_code',
|
||||
function(doc) {
|
||||
@ -651,15 +650,8 @@ frappe.ui.form.on("BOM Item", "items_remove", function(frm) {
|
||||
erpnext.bom.calculate_total(frm.doc);
|
||||
});
|
||||
|
||||
var toggle_operations = function(frm) {
|
||||
frm.toggle_display("operations_section", cint(frm.doc.with_operations) == 1);
|
||||
frm.toggle_display("transfer_material_against", cint(frm.doc.with_operations) == 1);
|
||||
frm.toggle_reqd("transfer_material_against", cint(frm.doc.with_operations) == 1);
|
||||
};
|
||||
|
||||
frappe.ui.form.on("BOM", "with_operations", function(frm) {
|
||||
if(!cint(frm.doc.with_operations)) {
|
||||
frm.set_value("operations", []);
|
||||
}
|
||||
toggle_operations(frm);
|
||||
});
|
@ -193,6 +193,7 @@
|
||||
},
|
||||
{
|
||||
"default": "Work Order",
|
||||
"depends_on": "with_operations",
|
||||
"fieldname": "transfer_material_against",
|
||||
"fieldtype": "Select",
|
||||
"label": "Transfer Material Against",
|
||||
@ -235,6 +236,7 @@
|
||||
{
|
||||
"fieldname": "operations_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1,
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
@ -245,6 +247,7 @@
|
||||
"options": "Routing"
|
||||
},
|
||||
{
|
||||
"depends_on": "with_operations",
|
||||
"fieldname": "operations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Operations",
|
||||
@ -517,7 +520,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-05-21 12:29:32.634952",
|
||||
"modified": "2021-03-16 12:25:09.081968",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
@ -81,7 +81,7 @@ class BOM(WebsiteGenerator):
|
||||
self.validate_operations()
|
||||
self.calculate_cost()
|
||||
self.update_stock_qty()
|
||||
self.update_cost(update_parent=False, from_child_bom=True, save=False)
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
|
||||
|
||||
def get_context(self, context):
|
||||
context.parents = [{'name': 'boms', 'title': _('All BOMs') }]
|
||||
@ -213,7 +213,7 @@ class BOM(WebsiteGenerator):
|
||||
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_cost(self, update_parent=True, from_child_bom=False, save=True):
|
||||
def update_cost(self, update_parent=True, from_child_bom=False, update_hour_rate = True, save=True):
|
||||
if self.docstatus == 2:
|
||||
return
|
||||
|
||||
@ -242,7 +242,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
if self.docstatus == 1:
|
||||
self.flags.ignore_validate_update_after_submit = True
|
||||
self.calculate_cost()
|
||||
self.calculate_cost(update_hour_rate)
|
||||
if save:
|
||||
self.db_update()
|
||||
|
||||
@ -403,32 +403,47 @@ class BOM(WebsiteGenerator):
|
||||
bom_list.reverse()
|
||||
return bom_list
|
||||
|
||||
def calculate_cost(self):
|
||||
def calculate_cost(self, update_hour_rate = False):
|
||||
"""Calculate bom totals"""
|
||||
self.calculate_op_cost()
|
||||
self.calculate_op_cost(update_hour_rate)
|
||||
self.calculate_rm_cost()
|
||||
self.calculate_sm_cost()
|
||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||
self.base_total_cost = self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||
|
||||
def calculate_op_cost(self):
|
||||
def calculate_op_cost(self, update_hour_rate = False):
|
||||
"""Update workstation rate and calculates totals"""
|
||||
self.operating_cost = 0
|
||||
self.base_operating_cost = 0
|
||||
for d in self.get('operations'):
|
||||
if d.workstation:
|
||||
if not d.hour_rate:
|
||||
hour_rate = flt(frappe.db.get_value("Workstation", d.workstation, "hour_rate"))
|
||||
d.hour_rate = hour_rate / flt(self.conversion_rate) if self.conversion_rate else hour_rate
|
||||
|
||||
if d.hour_rate and d.time_in_mins:
|
||||
d.base_hour_rate = flt(d.hour_rate) * flt(self.conversion_rate)
|
||||
d.operating_cost = flt(d.hour_rate) * flt(d.time_in_mins) / 60.0
|
||||
d.base_operating_cost = flt(d.operating_cost) * flt(self.conversion_rate)
|
||||
self.update_rate_and_time(d, update_hour_rate)
|
||||
|
||||
self.operating_cost += flt(d.operating_cost)
|
||||
self.base_operating_cost += flt(d.base_operating_cost)
|
||||
|
||||
def update_rate_and_time(self, row, update_hour_rate = False):
|
||||
if not row.hour_rate or update_hour_rate:
|
||||
hour_rate = flt(frappe.get_cached_value("Workstation", row.workstation, "hour_rate"))
|
||||
row.hour_rate = (hour_rate / flt(self.conversion_rate)
|
||||
if self.conversion_rate and hour_rate else hour_rate)
|
||||
|
||||
if self.routing:
|
||||
row.time_in_mins = flt(frappe.db.get_value("BOM Operation", {
|
||||
"workstation": row.workstation,
|
||||
"operation": row.operation,
|
||||
"sequence_id": row.sequence_id,
|
||||
"parent": self.routing
|
||||
}, ["time_in_mins"]))
|
||||
|
||||
if row.hour_rate and row.time_in_mins:
|
||||
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
||||
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
||||
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
|
||||
|
||||
if update_hour_rate:
|
||||
row.db_update()
|
||||
|
||||
def calculate_rm_cost(self):
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
total_rm_cost = 0
|
||||
@ -575,7 +590,7 @@ class BOM(WebsiteGenerator):
|
||||
self.get_routing()
|
||||
|
||||
def validate_operations(self):
|
||||
if self.with_operations and not self.get('operations'):
|
||||
if self.with_operations and not self.get('operations') and self.docstatus == 1:
|
||||
frappe.throw(_("Operations cannot be left blank"))
|
||||
|
||||
if self.with_operations:
|
||||
|
@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update
|
||||
from six import string_types
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||
|
||||
test_records = frappe.get_test_records('BOM')
|
||||
|
||||
@ -122,7 +123,7 @@ class TestBOM(unittest.TestCase):
|
||||
bom.items[0].conversion_factor = 5
|
||||
bom.insert()
|
||||
|
||||
bom.update_cost()
|
||||
bom.update_cost(update_hour_rate = False)
|
||||
|
||||
# test amounts in selected currency
|
||||
self.assertEqual(bom.items[0].rate, 300)
|
||||
@ -160,6 +161,7 @@ class TestBOM(unittest.TestCase):
|
||||
|
||||
def test_subcontractor_sourced_item(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
set_backflush_based_on('Material Transferred for Subcontract')
|
||||
|
||||
if not frappe.db.exists('Item', item_code):
|
||||
make_item(item_code, {
|
||||
|
@ -13,10 +13,10 @@
|
||||
"col_break1",
|
||||
"hour_rate",
|
||||
"time_in_mins",
|
||||
"batch_size",
|
||||
"operating_cost",
|
||||
"base_hour_rate",
|
||||
"base_operating_cost",
|
||||
"batch_size",
|
||||
"image"
|
||||
],
|
||||
"fields": [
|
||||
@ -61,6 +61,8 @@
|
||||
},
|
||||
{
|
||||
"description": "In minutes",
|
||||
"fetch_from": "operation.total_operation_time",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "time_in_mins",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
@ -104,7 +106,8 @@
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fetch_from": "operation.batch_size",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "batch_size",
|
||||
"fieldtype": "Int",
|
||||
"label": "Batch Size"
|
||||
@ -120,7 +123,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-13 18:14:10.018774",
|
||||
"modified": "2021-01-12 14:48:09.596843",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
|
@ -11,6 +11,16 @@ frappe.ui.form.on('Job Card', {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_indicator_formatter('sub_operation',
|
||||
function(doc) {
|
||||
if (doc.status == "Pending") {
|
||||
return "red";
|
||||
} else {
|
||||
return doc.status === "Complete" ? "green" : "orange";
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
@ -31,6 +41,10 @@ frappe.ui.form.on('Job Card', {
|
||||
}
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus == 1 && !frm.doc.is_corrective_job_card) {
|
||||
frm.trigger('setup_corrective_job_card');
|
||||
}
|
||||
|
||||
frm.set_query("quality_inspection", function() {
|
||||
return {
|
||||
query: "erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query",
|
||||
@ -43,12 +57,62 @@ 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)
|
||||
if (frm.doc.docstatus == 0 && !frm.is_new() &&
|
||||
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
|
||||
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||
frm.trigger("prepare_timer_buttons");
|
||||
}
|
||||
},
|
||||
|
||||
setup_corrective_job_card: function(frm) {
|
||||
frm.add_custom_button(__('Corrective Job Card'), () => {
|
||||
let operations = frm.doc.sub_operations.map(d => d.sub_operation).concat(frm.doc.operation);
|
||||
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: 'Link', label: __('Corrective Operation'), options: 'Operation',
|
||||
fieldname: 'operation', get_query() {
|
||||
return {
|
||||
filters: {
|
||||
"is_corrective_operation": 1
|
||||
}
|
||||
};
|
||||
}
|
||||
}, {
|
||||
fieldtype: 'Link', label: __('For Operation'), options: 'Operation',
|
||||
fieldname: 'for_operation', get_query() {
|
||||
return {
|
||||
filters: {
|
||||
"name": ["in", operations]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
frappe.prompt(fields, d => {
|
||||
frm.events.make_corrective_job_card(frm, d.operation, d.for_operation);
|
||||
}, __("Select Corrective Operation"));
|
||||
}, __('Make'));
|
||||
},
|
||||
|
||||
make_corrective_job_card: function(frm, operation, for_operation) {
|
||||
frappe.call({
|
||||
method: 'erpnext.manufacturing.doctype.job_card.job_card.make_corrective_job_card',
|
||||
args: {
|
||||
source_name: frm.doc.name,
|
||||
operation: operation,
|
||||
for_operation: for_operation
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
operation: function(frm) {
|
||||
frm.trigger("toggle_operation_number");
|
||||
|
||||
@ -97,101 +161,105 @@ frappe.ui.form.on('Job Card', {
|
||||
|
||||
prepare_timer_buttons: function(frm) {
|
||||
frm.trigger("make_dashboard");
|
||||
if (!frm.doc.job_started) {
|
||||
frm.add_custom_button(__("Start"), () => {
|
||||
if (!frm.doc.employee) {
|
||||
frappe.prompt({fieldtype: 'Link', label: __('Employee'), options: "Employee",
|
||||
fieldname: 'employee'}, d => {
|
||||
if (d.employee) {
|
||||
frm.set_value("employee", d.employee);
|
||||
|
||||
if (!frm.doc.started_time && !frm.doc.current_time) {
|
||||
frm.add_custom_button(__("Start Job"), () => {
|
||||
if ((frm.doc.employee && !frm.doc.employee.length) || !frm.doc.employee) {
|
||||
frappe.prompt({fieldtype: 'Table MultiSelect', label: __('Select Employees'),
|
||||
options: "Job Card Time Log", fieldname: 'employees'}, d => {
|
||||
frm.events.start_job(frm, "Work In Progress", d.employees);
|
||||
}, __("Assign Job to Employee"));
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
}
|
||||
}, __("Enter Value"), __("Start"));
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
frm.events.start_job(frm, "Work In Progress", frm.doc.employee);
|
||||
}
|
||||
}).addClass("btn-primary");
|
||||
} else if (frm.doc.status == "On Hold") {
|
||||
frm.add_custom_button(__("Resume"), () => {
|
||||
frappe.flags.resume_job = 1;
|
||||
frm.events.start_job(frm);
|
||||
frm.add_custom_button(__("Resume Job"), () => {
|
||||
frm.events.start_job(frm, "Resume Job", frm.doc.employee);
|
||||
}).addClass("btn-primary");
|
||||
} else {
|
||||
frm.add_custom_button(__("Pause"), () => {
|
||||
frappe.flags.pause_job = 1;
|
||||
frm.set_value("status", "On Hold");
|
||||
frm.events.complete_job(frm);
|
||||
frm.add_custom_button(__("Pause Job"), () => {
|
||||
frm.events.complete_job(frm, "On Hold");
|
||||
});
|
||||
|
||||
frm.add_custom_button(__("Complete"), () => {
|
||||
let completed_time = frappe.datetime.now_datetime();
|
||||
frm.trigger("hide_timer");
|
||||
frm.add_custom_button(__("Complete Job"), () => {
|
||||
var sub_operations = frm.doc.sub_operations;
|
||||
|
||||
if (frm.doc.for_quantity) {
|
||||
let set_qty = true;
|
||||
if (sub_operations && sub_operations.length > 1) {
|
||||
set_qty = false;
|
||||
let last_op_row = sub_operations[sub_operations.length - 2];
|
||||
|
||||
if (last_op_row.status == 'Complete') {
|
||||
set_qty = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (set_qty) {
|
||||
frappe.prompt({fieldtype: 'Float', label: __('Completed Quantity'),
|
||||
fieldname: 'qty', reqd: 1, default: frm.doc.for_quantity}, data => {
|
||||
frm.events.complete_job(frm, completed_time, data.qty);
|
||||
}, __("Enter Value"), __("Complete"));
|
||||
fieldname: 'qty', default: frm.doc.for_quantity}, data => {
|
||||
frm.events.complete_job(frm, "Complete", data.qty);
|
||||
}, __("Enter Value"));
|
||||
} else {
|
||||
frm.events.complete_job(frm, completed_time, 0);
|
||||
frm.events.complete_job(frm, "Complete", 0.0);
|
||||
}
|
||||
}).addClass("btn-primary");
|
||||
}
|
||||
},
|
||||
|
||||
start_job: function(frm) {
|
||||
let row = frappe.model.add_child(frm.doc, 'Job Card Time Log', 'time_logs');
|
||||
row.from_time = frappe.datetime.now_datetime();
|
||||
frm.set_value('job_started', 1);
|
||||
frm.set_value('started_time' , row.from_time);
|
||||
frm.set_value("status", "Work In Progress");
|
||||
|
||||
if (!frappe.flags.resume_job) {
|
||||
frm.set_value('current_time' , 0);
|
||||
}
|
||||
|
||||
frm.save();
|
||||
start_job: function(frm, status, employee) {
|
||||
const args = {
|
||||
job_card_id: frm.doc.name,
|
||||
start_time: frappe.datetime.now_datetime(),
|
||||
employees: employee,
|
||||
status: status
|
||||
};
|
||||
frm.events.make_time_log(frm, args);
|
||||
},
|
||||
|
||||
complete_job: function(frm, completed_time, completed_qty) {
|
||||
frm.doc.time_logs.forEach(d => {
|
||||
if (d.from_time && !d.to_time) {
|
||||
d.to_time = completed_time || frappe.datetime.now_datetime();
|
||||
d.completed_qty = completed_qty || 0;
|
||||
complete_job: function(frm, status, completed_qty) {
|
||||
const args = {
|
||||
job_card_id: frm.doc.name,
|
||||
complete_time: frappe.datetime.now_datetime(),
|
||||
status: status,
|
||||
completed_qty: completed_qty
|
||||
};
|
||||
frm.events.make_time_log(frm, args);
|
||||
},
|
||||
|
||||
if(frappe.flags.pause_job) {
|
||||
let currentIncrement = moment(d.to_time).diff(moment(d.from_time),"seconds") || 0;
|
||||
frm.set_value('current_time' , currentIncrement + (frm.doc.current_time || 0));
|
||||
} else {
|
||||
frm.set_value('started_time' , '');
|
||||
frm.set_value('job_started', 0);
|
||||
frm.set_value('current_time' , 0);
|
||||
}
|
||||
make_time_log: function(frm, args) {
|
||||
frm.events.update_sub_operation(frm, args);
|
||||
|
||||
frm.save();
|
||||
frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.job_card.job_card.make_time_log",
|
||||
args: {
|
||||
args: args
|
||||
},
|
||||
freeze: true,
|
||||
callback: function () {
|
||||
frm.reload_doc();
|
||||
frm.trigger("make_dashboard");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
update_sub_operation: function(frm, args) {
|
||||
if (frm.doc.sub_operations && frm.doc.sub_operations.length) {
|
||||
let sub_operations = frm.doc.sub_operations.filter(d => d.status != 'Complete');
|
||||
if (sub_operations && sub_operations.length) {
|
||||
args["sub_operation"] = sub_operations[0].sub_operation;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
validate: function(frm) {
|
||||
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
|
||||
frm.trigger("reset_timer");
|
||||
}
|
||||
},
|
||||
|
||||
employee: function(frm) {
|
||||
if (frm.doc.job_started && !frm.doc.current_time) {
|
||||
frm.trigger("reset_timer");
|
||||
} else {
|
||||
frm.events.start_job(frm);
|
||||
}
|
||||
},
|
||||
|
||||
reset_timer: function(frm) {
|
||||
frm.set_value('started_time' , '');
|
||||
frm.set_value('job_started', 0);
|
||||
frm.set_value('current_time' , 0);
|
||||
},
|
||||
|
||||
make_dashboard: function(frm) {
|
||||
@ -297,7 +365,6 @@ frappe.ui.form.on('Job Card Time Log', {
|
||||
},
|
||||
|
||||
to_time: function(frm) {
|
||||
frm.set_value('job_started', 0);
|
||||
frm.set_value('started_time', '');
|
||||
}
|
||||
})
|
@ -9,38 +9,49 @@
|
||||
"naming_series",
|
||||
"work_order",
|
||||
"bom_no",
|
||||
"workstation",
|
||||
"operation",
|
||||
"operation_row_number",
|
||||
"column_break_4",
|
||||
"posting_date",
|
||||
"company",
|
||||
"remarks",
|
||||
"production_section",
|
||||
"production_item",
|
||||
"item_name",
|
||||
"for_quantity",
|
||||
"quality_inspection",
|
||||
"wip_warehouse",
|
||||
"serial_no",
|
||||
"column_break_12",
|
||||
"employee",
|
||||
"employee_name",
|
||||
"status",
|
||||
"wip_warehouse",
|
||||
"quality_inspection",
|
||||
"project",
|
||||
"batch_no",
|
||||
"operation_section_section",
|
||||
"operation",
|
||||
"operation_row_number",
|
||||
"column_break_18",
|
||||
"workstation",
|
||||
"employee",
|
||||
"section_break_21",
|
||||
"sub_operations",
|
||||
"timing_detail",
|
||||
"time_logs",
|
||||
"section_break_13",
|
||||
"total_completed_qty",
|
||||
"total_time_in_mins",
|
||||
"column_break_15",
|
||||
"total_time_in_mins",
|
||||
"section_break_8",
|
||||
"items",
|
||||
"corrective_operation_section",
|
||||
"for_job_card",
|
||||
"is_corrective_job_card",
|
||||
"column_break_33",
|
||||
"hour_rate",
|
||||
"for_operation",
|
||||
"more_information",
|
||||
"operation_id",
|
||||
"sequence_id",
|
||||
"transferred_qty",
|
||||
"requested_qty",
|
||||
"status",
|
||||
"column_break_20",
|
||||
"remarks",
|
||||
"barcode",
|
||||
"job_started",
|
||||
"started_time",
|
||||
@ -117,13 +128,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Timing Detail"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "time_logs",
|
||||
@ -133,9 +137,11 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_13",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "total_completed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Completed Qty",
|
||||
@ -160,8 +166,7 @@
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"label": "Items",
|
||||
"options": "Job Card Item",
|
||||
"read_only": 1
|
||||
"options": "Job Card Item"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@ -251,12 +256,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Employee Name"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "production_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Production"
|
||||
@ -314,11 +314,89 @@
|
||||
"label": "Quality Inspection",
|
||||
"no_copy": 1,
|
||||
"options": "Quality Inspection"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 1,
|
||||
"fieldname": "sub_operations",
|
||||
"fieldtype": "Table",
|
||||
"label": "Sub Operations",
|
||||
"options": "Job Card Operation",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Operation Section"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_21",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_border": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "is_corrective_job_card",
|
||||
"fieldname": "hour_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Hour Rate"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "is_corrective_job_card",
|
||||
"fieldname": "corrective_operation_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Corrective Operation"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_corrective_job_card",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Corrective Job Card",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_33",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "for_job_card",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Job Card",
|
||||
"options": "Job Card",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "for_job_card.operation",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "for_operation",
|
||||
"fieldtype": "Link",
|
||||
"label": "For Operation",
|
||||
"options": "Operation"
|
||||
},
|
||||
{
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Employee",
|
||||
"options": "Job Card Time Log"
|
||||
},
|
||||
{
|
||||
"fieldname": "serial_no",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Serial No"
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "Batch No",
|
||||
"options": "Batch"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-19 18:26:50.531664",
|
||||
"modified": "2021-03-16 15:59:32.766484",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
@ -5,11 +5,12 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
import datetime
|
||||
import json
|
||||
from frappe import _, bold
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
|
||||
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
|
||||
get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form, time_diff_in_seconds)
|
||||
|
||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
|
||||
|
||||
@ -25,10 +26,21 @@ class JobCard(Document):
|
||||
self.set_status()
|
||||
self.validate_operation_id()
|
||||
self.validate_sequence_id()
|
||||
self.get_sub_operations()
|
||||
self.update_sub_operation_status()
|
||||
|
||||
def get_sub_operations(self):
|
||||
if self.operation:
|
||||
self.sub_operations = []
|
||||
for row in frappe.get_all("Sub Operation",
|
||||
filters = {"parent": self.operation}, fields=["operation", "idx"]):
|
||||
row.status = "Pending"
|
||||
row.sub_operation = row.operation
|
||||
self.append("sub_operations", row)
|
||||
|
||||
def validate_time_logs(self):
|
||||
self.total_completed_qty = 0.0
|
||||
self.total_time_in_mins = 0.0
|
||||
self.total_completed_qty = 0.0
|
||||
|
||||
if self.get('time_logs'):
|
||||
for d in self.get('time_logs'):
|
||||
@ -44,11 +56,14 @@ class JobCard(Document):
|
||||
d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60
|
||||
self.total_time_in_mins += d.time_in_mins
|
||||
|
||||
if d.completed_qty:
|
||||
if d.completed_qty and not self.sub_operations:
|
||||
self.total_completed_qty += d.completed_qty
|
||||
|
||||
self.total_completed_qty = flt(self.total_completed_qty, self.precision("total_completed_qty"))
|
||||
|
||||
for row in self.sub_operations:
|
||||
self.total_completed_qty += row.completed_qty
|
||||
|
||||
def get_overlap_for(self, args, check_next_available_slot=False):
|
||||
production_capacity = 1
|
||||
|
||||
@ -57,7 +72,7 @@ class JobCard(Document):
|
||||
self.workstation, 'production_capacity') or 1
|
||||
validate_overlap_for = " and jc.workstation = %(workstation)s "
|
||||
|
||||
if self.employee:
|
||||
if args.get("employee"):
|
||||
# override capacity for employee
|
||||
production_capacity = 1
|
||||
validate_overlap_for = " and jc.employee = %(employee)s "
|
||||
@ -80,7 +95,7 @@ class JobCard(Document):
|
||||
"to_time": args.to_time,
|
||||
"name": args.name or "No Name",
|
||||
"parent": args.parent or "No Name",
|
||||
"employee": self.employee,
|
||||
"employee": args.get("employee"),
|
||||
"workstation": self.workstation
|
||||
}, as_dict=True)
|
||||
|
||||
@ -158,6 +173,100 @@ class JobCard(Document):
|
||||
row.planned_start_time = datetime.datetime.combine(start_date,
|
||||
get_time(workstation_doc.working_hours[0].start_time))
|
||||
|
||||
def add_time_log(self, args):
|
||||
last_row = []
|
||||
employees = args.employees
|
||||
if isinstance(employees, str):
|
||||
employees = json.loads(employees)
|
||||
|
||||
if self.time_logs and len(self.time_logs) > 0:
|
||||
last_row = self.time_logs[-1]
|
||||
|
||||
self.reset_timer_value(args)
|
||||
if last_row and args.get("complete_time"):
|
||||
for row in self.time_logs:
|
||||
if not row.to_time:
|
||||
row.update({
|
||||
"to_time": get_datetime(args.get("complete_time")),
|
||||
"operation": args.get("sub_operation"),
|
||||
"completed_qty": args.get("completed_qty") or 0.0
|
||||
})
|
||||
elif args.get("start_time"):
|
||||
for name in employees:
|
||||
self.append("time_logs", {
|
||||
"from_time": get_datetime(args.get("start_time")),
|
||||
"employee": name.get('employee'),
|
||||
"operation": args.get("sub_operation"),
|
||||
"completed_qty": 0.0
|
||||
})
|
||||
|
||||
if not self.employee:
|
||||
self.set_employees(employees)
|
||||
|
||||
if self.status == "On Hold":
|
||||
self.current_time = time_diff_in_seconds(last_row.to_time, last_row.from_time)
|
||||
|
||||
self.save()
|
||||
|
||||
def set_employees(self, employees):
|
||||
for name in employees:
|
||||
self.append('employee', {
|
||||
'employee': name.get('employee'),
|
||||
'completed_qty': 0.0
|
||||
})
|
||||
|
||||
def reset_timer_value(self, args):
|
||||
self.started_time = None
|
||||
|
||||
if args.get("status") in ["Work In Progress", "Complete"]:
|
||||
self.current_time = 0.0
|
||||
|
||||
if args.get("status") == "Work In Progress":
|
||||
self.started_time = get_datetime(args.get("start_time"))
|
||||
|
||||
if args.get("status") == "Resume Job":
|
||||
args["status"] = "Work In Progress"
|
||||
|
||||
if args.get("status"):
|
||||
self.status = args.get("status")
|
||||
|
||||
def update_sub_operation_status(self):
|
||||
if not (self.sub_operations and self.time_logs):
|
||||
return
|
||||
|
||||
operation_wise_completed_time = {}
|
||||
for time_log in self.time_logs:
|
||||
if time_log.operation not in operation_wise_completed_time:
|
||||
operation_wise_completed_time.setdefault(time_log.operation,
|
||||
frappe._dict({"status": "Pending", "completed_qty":0.0, "completed_time": 0.0, "employee": []}))
|
||||
|
||||
op_row = operation_wise_completed_time[time_log.operation]
|
||||
op_row.status = "Work In Progress" if not time_log.time_in_mins else "Complete"
|
||||
if self.status == 'On Hold':
|
||||
op_row.status = 'Pause'
|
||||
|
||||
op_row.employee.append(time_log.employee)
|
||||
if time_log.time_in_mins:
|
||||
op_row.completed_time += time_log.time_in_mins
|
||||
op_row.completed_qty += time_log.completed_qty
|
||||
|
||||
for row in self.sub_operations:
|
||||
operation_deatils = operation_wise_completed_time.get(row.sub_operation)
|
||||
if operation_deatils:
|
||||
if row.status != 'Complete':
|
||||
row.status = operation_deatils.status
|
||||
|
||||
row.completed_time = operation_deatils.completed_time
|
||||
if operation_deatils.employee:
|
||||
row.completed_time = row.completed_time / len(set(operation_deatils.employee))
|
||||
|
||||
if operation_deatils.completed_qty:
|
||||
row.completed_qty = operation_deatils.completed_qty / len(set(operation_deatils.employee))
|
||||
else:
|
||||
row.status = 'Pending'
|
||||
row.completed_time = 0.0
|
||||
row.completed_qty = 0.0
|
||||
|
||||
def update_time_logs(self, row):
|
||||
self.append("time_logs", {
|
||||
"from_time": row.planned_start_time,
|
||||
@ -182,15 +291,18 @@ class JobCard(Document):
|
||||
|
||||
if self.get('operation') == d.operation:
|
||||
self.append('items', {
|
||||
'item_code': d.item_code,
|
||||
'source_warehouse': d.source_warehouse,
|
||||
'uom': frappe.db.get_value("Item", d.item_code, 'stock_uom'),
|
||||
'item_name': d.item_name,
|
||||
'description': d.description,
|
||||
'required_qty': (d.required_qty * flt(self.for_quantity)) / doc.qty
|
||||
"item_code": d.item_code,
|
||||
"source_warehouse": d.source_warehouse,
|
||||
"uom": frappe.db.get_value("Item", d.item_code, 'stock_uom'),
|
||||
"item_name": d.item_name,
|
||||
"description": d.description,
|
||||
"required_qty": (d.required_qty * flt(self.for_quantity)) / doc.qty,
|
||||
"rate": d.rate,
|
||||
"amount": d.amount
|
||||
})
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_transfer_qty()
|
||||
self.validate_job_card()
|
||||
self.update_work_order()
|
||||
self.set_transferred_qty()
|
||||
@ -199,7 +311,16 @@ class JobCard(Document):
|
||||
self.update_work_order()
|
||||
self.set_transferred_qty()
|
||||
|
||||
def validate_transfer_qty(self):
|
||||
if self.items and self.transferred_qty < self.for_quantity:
|
||||
frappe.throw(_('Materials needs to be transferred to the work in progress warehouse for the job card {0}')
|
||||
.format(self.name))
|
||||
|
||||
def validate_job_card(self):
|
||||
if self.work_order and frappe.get_cached_value('Work Order', self.work_order, 'status') == 'Stopped':
|
||||
frappe.throw(_("Transaction not allowed against stopped Work Order {0}")
|
||||
.format(get_link_to_form('Work Order', self.work_order)))
|
||||
|
||||
if not self.time_logs:
|
||||
frappe.throw(_("Time logs are required for {0} {1}")
|
||||
.format(bold("Job Card"), get_link_to_form("Job Card", self.name)))
|
||||
@ -215,6 +336,10 @@ class JobCard(Document):
|
||||
if not self.work_order:
|
||||
return
|
||||
|
||||
if self.is_corrective_job_card and not cint(frappe.db.get_single_value('Manufacturing Settings',
|
||||
'add_corrective_operation_cost_in_finished_good_valuation')):
|
||||
return
|
||||
|
||||
for_quantity, time_in_mins = 0, 0
|
||||
from_time_list, to_time_list = [], []
|
||||
|
||||
@ -225,10 +350,24 @@ class JobCard(Document):
|
||||
time_in_mins = flt(data[0].time_in_mins)
|
||||
|
||||
wo = frappe.get_doc('Work Order', self.work_order)
|
||||
if self.operation_id:
|
||||
|
||||
if self.is_corrective_job_card:
|
||||
self.update_corrective_in_work_order(wo)
|
||||
|
||||
elif self.operation_id:
|
||||
self.validate_produced_quantity(for_quantity, wo)
|
||||
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
||||
|
||||
def update_corrective_in_work_order(self, wo):
|
||||
wo.corrective_operation_cost = 0.0
|
||||
for row in frappe.get_all('Job Card', fields = ['total_time_in_mins', 'hour_rate'],
|
||||
filters = {'is_corrective_job_card': 1, 'docstatus': 1, 'work_order': self.work_order}):
|
||||
wo.corrective_operation_cost += flt(row.total_time_in_mins) * flt(row.hour_rate)
|
||||
|
||||
wo.calculate_operating_cost()
|
||||
wo.flags.ignore_validate_update_after_submit = True
|
||||
wo.save()
|
||||
|
||||
def validate_produced_quantity(self, for_quantity, wo):
|
||||
if self.docstatus < 2: return
|
||||
|
||||
@ -248,8 +387,8 @@ class JobCard(Document):
|
||||
min(from_time) as start_time, max(to_time) as end_time
|
||||
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
||||
WHERE
|
||||
jctl.parent = jc.name and jc.work_order = %s
|
||||
and jc.operation_id = %s and jc.docstatus = 1
|
||||
jctl.parent = jc.name and jc.work_order = %s and jc.operation_id = %s
|
||||
and jc.docstatus = 1 and IFNULL(jc.is_corrective_job_card, 0) = 0
|
||||
""", (self.work_order, self.operation_id), as_dict=1)
|
||||
|
||||
for data in wo.operations:
|
||||
@ -271,7 +410,8 @@ class JobCard(Document):
|
||||
def get_current_operation_data(self):
|
||||
return 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, "operation_id": self.operation_id})
|
||||
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id,
|
||||
"is_corrective_job_card": 0})
|
||||
|
||||
def set_transferred_qty_in_job_card(self, ste_doc):
|
||||
for row in ste_doc.items:
|
||||
@ -317,7 +457,7 @@ class JobCard(Document):
|
||||
'docstatus': ('!=', 2)}, fields = 'sum(transferred_qty) as qty', group_by='operation_id')
|
||||
|
||||
if job_cards:
|
||||
qty = min([d.qty for d in job_cards])
|
||||
qty = min(d.qty for d in job_cards)
|
||||
|
||||
doc.db_set('material_transferred_for_manufacturing', qty)
|
||||
|
||||
@ -354,7 +494,11 @@ class JobCard(Document):
|
||||
.format(bold(self.operation), work_order), OperationMismatchError)
|
||||
|
||||
def validate_sequence_id(self):
|
||||
if not (self.work_order and self.sequence_id): return
|
||||
if self.is_corrective_job_card:
|
||||
return
|
||||
|
||||
if not (self.work_order and self.sequence_id):
|
||||
return
|
||||
|
||||
current_operation_qty = 0.0
|
||||
data = self.get_current_operation_data()
|
||||
@ -376,6 +520,17 @@ class JobCard(Document):
|
||||
frappe.throw(_("{0}, complete the operation {1} before the operation {2}.")
|
||||
.format(message, bold(row.operation), bold(self.operation)), OperationSequenceError)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_time_log(args):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
args = frappe._dict(args)
|
||||
doc = frappe.get_doc("Job Card", args.job_card_id)
|
||||
doc.validate_sequence_id()
|
||||
doc.add_time_log(args)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_operation_details(work_order, operation):
|
||||
if work_order and operation:
|
||||
@ -511,3 +666,28 @@ def get_job_details(start, end, filters=None):
|
||||
events.append(job_card_data)
|
||||
|
||||
return events
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_corrective_job_card(source_name, operation=None, for_operation=None, target_doc=None):
|
||||
def set_missing_values(source, target):
|
||||
target.is_corrective_job_card = 1
|
||||
target.operation = operation
|
||||
target.for_operation = for_operation
|
||||
|
||||
target.set('time_logs', [])
|
||||
target.set('employee', [])
|
||||
target.set('items', [])
|
||||
target.get_sub_operations()
|
||||
target.get_required_items()
|
||||
target.validate_time_logs()
|
||||
|
||||
doclist = get_mapped_doc("Job Card", source_name, {
|
||||
"Job Card": {
|
||||
"doctype": "Job Card",
|
||||
"field_map": {
|
||||
"name": "for_job_card",
|
||||
},
|
||||
}
|
||||
}, target_doc, set_missing_values)
|
||||
|
||||
return doclist
|
@ -25,8 +25,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Code",
|
||||
"options": "Item",
|
||||
"read_only": 1
|
||||
"options": "Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "source_warehouse",
|
||||
@ -67,8 +66,7 @@
|
||||
"fieldname": "required_qty",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Required Qty",
|
||||
"read_only": 1
|
||||
"label": "Required Qty"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
@ -107,7 +105,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-11 13:50:13.804108",
|
||||
"modified": "2021-04-22 18:50:00.003444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Item",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user